Skip to content

Instantly share code, notes, and snippets.

@Dizro
Created July 3, 2026 22:58
Show Gist options
  • Select an option

  • Save Dizro/1c315cdeb09f1bdb9a6d73bf07129595 to your computer and use it in GitHub Desktop.

Select an option

Save Dizro/1c315cdeb09f1bdb9a6d73bf07129595 to your computer and use it in GitHub Desktop.

VK Music Cleaner

Мощный userscript-стиля скрипт для полного удаления всей музыки из раздела «Моя музыка» ВКонтакте. Запускается прямо из консоли браузера — без расширений, без установок, без серверов.

⚠️ Дисклеймер: Скрипт автоматизирует пользовательский интерфейс VK. Используйте на свой риск. Автор не несёт ответственности за возможные последствия, включая ограничения со стороны платформы. Удаление музыки — необратимое действие.

Возможности

  • Без перезагрузок — работает в одной вкладке от начала до конца.
  • Прямой вызов onClick из React Fiber — надёжнее, чем симуляция событий мыши.
  • Плавная адаптация скорости (−1/+1) вместо агрессивных качелей.
  • Авто-поиск скролл-контейнера — корректно работает с виртуализацией VK.
  • Перехват bad_hash из fetch и XMLHttpRequest с экспоненциальной паузой.
  • Skip-list: «залипшие» треки пропускаются после нескольких попыток.
  • UI-оверлей с прогрессом, скоростью (треков/сек) и ETA.
  • Горячие клавиши: Ctrl+Shift+P — пауза, Ctrl+Shift+R — продолжить, Ctrl+Shift+S — стоп.
  • Защита от случайного закрытия вкладки.
  • Тихий режим — логирует только ошибки и milestones каждые 50 треков.
  • Persisted-стейт: счётчик сохраняется в localStorage, переживает перезапуск.

Использование

Шаг 1. Откройте «Мою музыку»

Перейдите на https://vk.com/audio и дождитесь полной загрузки списка треков.

Шаг 2. Откройте консоль браузера

Браузер Сочетание
Chrome / Edge / Brave F12 или Ctrl+Shift+J (Windows/Linux), Cmd+Option+J (macOS)
Firefox F12 или Ctrl+Shift+K
Safari Cmd+Option+C (нужна активация Developer Menu в настройках)

Перейдите на вкладку Console.

Шаг 3. Вставьте код

Скопируйте всё содержимое файла vk-music-cleaner.js и вставьте в консоль. Нажмите Enter.

В правом верхнем углу появится оверлей с прогрессом.

Шаг 4. Не закрывайте вкладку

Скрипт работает в фоне. Можно свернуть вкладку, но не закрывать. Оверлей показывает:

  • сколько треков удалено (всего и за сессию),
  • текущую скорость (треков/сек),
  • ETA до чекпойнта,
  • текущий размер батча и паузу.

Управление

Действие Способ
Пауза Кнопка «⏸ Пауза» в оверлее или Ctrl+Shift+P
Продолжить Кнопка «▶ Продолжить» или Ctrl+Shift+R
Полная остановка Кнопка «⏹ Стоп» или Ctrl+Shift+S
Переместить оверлей Перетащить мышью за любую точку

Настройка скорости

В начале файла найдите объект CONFIG:

const CONFIG = {
  speed: 'normal',    // 'safe' | 'normal' | 'turbo' | 'extreme'
  verbose: false,     // true — логировать каждый батч
  protectUnload: true,// предупреждать при закрытии вкладки
  // ...
};
Режим Батч Пауза Скорость Риск bad_hash
safe 3 2000 мс ~1.5 тр/сек минимальный
normal 5 1500 мс ~3.3 тр/сек низкий
turbo 8 1000 мс ~8 тр/сек средний
extreme 12 700 мс ~17 тр/сек высокий

При bad_hash скрипт автоматически замедляется на 1 шаг и плавно восстанавливается после 5 успешных батчей.

Команды в консоли

Сброс счётчика (начать с нуля)

localStorage.removeItem('vk_music_deleted');
localStorage.removeItem('vk_music_running');
localStorage.removeItem('vk_music_paused');

Проверить прогресс

console.log('Удалено:', localStorage.getItem('vk_music_deleted'));

Экстренная остановка

localStorage.setItem('vk_music_running', '0');

Затем обновите страницу.

Как это работает

  1. Поиск кнопок удаления — находит все элементы [data-testid="MusicAudio_ToggleOwning"][data-testactive="true"] (это React-компонент VK с активным состоянием «в моей музыке»).

  2. Активация через hover — для каждой строки трека диспатчится pointerenter/over/enter/over/move, чтобы виртуализированный React-компонент отрисовал кнопку.

  3. Умный клик через React Fiber — читает __reactProps$ на кнопке и вызывает onClick() напрямую с фейковым event-объектом. Это надёжнее, чем dispatchEvent(new MouseEvent(...)), потому что обходит проверки isTrusted.

  4. Пакетные клики — кликает по batchSize кнопок почти одновременно, потом ждёт batchPause мс.

  5. Подтверждение удаления — ждёт, пока строка исчезнет из DOM или кнопка сменит data-testactive на false. Если за 5 сек это не произошло — трек попадает в skip-list.

  6. Перехват bad_hash — подменяет window.fetch и XMLHttpRequest.prototype.send, читает тела ответов. Если видит bad_hash — взводит флаг, главный цикл переходит в режим замедления.

  7. Скролл — после каждого батча прокручивает контейнер списка на 800px, чтобы подгрузить следующие треки через lazy-loading.

Советы

  • Если bad_hash ловится часто — переключите speed на safe или уменьшите batchSize вручную в PROFILES.
  • Если скрипт «застрял» на низу списка — обновите страницу и запустите снова; счётчик продолжится с того же места.
  • Если оверлей мешает — просто перетащите его в удобное место, позиция не сохраняется.
  • Большая библиотека (>1000 треков) — лучше оставить на ночь в режиме safe, чтобы избежать частых bad_hash.

Ограничения

  • Скрипт удаляет треки только из «Моей музыки». Если нужно очистить плейлисты — откройте каждый плейлист отдельно и запустите скрипт там.
  • ВК может временно заблокировать аккаунт за «подозрительную активность» (показывает капчу). В таком случае сделайте паузу на 10–15 минут.
  • Скрипт не работает с мобильной версией VK (нужен desktop-режим в браузере).

FAQ

Q: Безопасно ли это? Могут ли забанить? A: Скрипт использует тот же API, что и обычный пользовательский интерфейс. Баны за очистку собственной музыки крайне редки, но ВК может показать капчу при агрессивном темпе. Используйте режим safe, если беспокоитесь.

Q: Можно ли восстановить удалённую музыку? A: Нет. Удаление из «Моей музыки» — необратимое действие. Перед запуском убедитесь, что действительно хотите очистить библиотеку.

Q: Скрипт пишет «bad_hash» — что делать? A: Ничего, скрипт сам замедлится и продолжит. Если bad_hash идёт постоянно — переключите speed на safe и перезапустите.

Q: Скрипт остановился на «низ списка». Что не так? A: Скорее всего, вы реально дошли до конца. Проверьте страницу визуально — если треки ещё есть, обновите страницу и запустите снова.

Q: Работает ли в мобильном Chrome? A: Да, если включить «Desktop site» в меню браузера. Но удобнее на компьютере.

Лицензия

MIT. Используйте, форкайте, модифицируйте freely.

Contributing

PR welcome! Особенно:

  • 🐛 Баг-репорты через Issues
  • 🚀 Новые режимы скорости
  • 🌍 Переводы UI на другие языки
  • 🔍 Улучшенная детекция VK API изменений

Changelog

  • v7.0 — React Fiber direct call, UI overlay, hotkeys, skip-list, плавный backoff.
  • v6.0 — Бесконечный цикл без лимитов, перехват bad_hash.
  • v5.0 — Убраны перезагрузки страницы.
  • v4.0 — Пакетные клики (batching).
  • v3.0 — Построчная обработка с hover.
  • v2.0 — Защита от bad_hash через задержки.
  • v1.0 — Базовая версия с последовательными кликами.
/*!
* VK Music Cleaner — удаляет всю музыку из раздела «Моя музыка» ВКонтакте.
*
* Возможности:
* • Без перезагрузок — работает в одной вкладке до конца.
* • Прямой выз onClick из React Fiber (надёжнее dispatchEvent).
* • Плавная адаптация скорости (−1/+1, а не /2/*2).
* • Авто-поиск правильного скролл-контейнера (виртуализация ВК).
* • Перехват bad_hash из fetch + XHR с экспоненциальной паузой.
* • Skip-list: «залипшие» кнопки пропускаются после 2 попыток.
* • UI-оверлей с прогрессом, скоростью и ETA.
* • Горячие клавиши: Ctrl+Shift+P (пауза), Ctrl+Shift+R (продолжить).
* • Защита от случайного закрытия вкладки (beforeunload).
* • Тихий режим (каждые 50 треков) + флаг verbose для отладки.
* • Статистика сессии сохраняется в localStorage.
*
* Лицензия: MIT. Используйте на свой риск — это автоматизация
* пользовательского интерфейса. Автор не несёт ответственности
* за последствия, включая возможные ограничения со стороны ВК.
*
* Использование:
* 1. Откройте https://vk.com/audio (раздел «Моя музыка»).
* 2. F12 → Console.
* 3. Вставьте этот код целиком → Enter.
* 4. Не закрывайте вкладку. Можно свернуть.
*
* Управление:
* Пауза / продолжить — кнопки в оверлее (правый верхний угол).
* Ctrl+Shift+P — пауза.
* Ctrl+Shift+R — продолжить.
* Ctrl+Shift+S — полная остановка + скрыть оверлей.
*
* Сброс состояния (в консоли):
* localStorage.removeItem('vk_music_deleted')
* localStorage.removeItem('vk_music_running')
* localStorage.removeItem('vk_music_paused')
*
* Скорость (поменять в объекте CONFIG):
* 'safe' — батч 3, пауза 2000 мс (максимально надёжно)
* 'normal' — батч 5, пауза 1500 мс (по умолчанию)
* 'turbo' — батч 8, пауза 1000 мс
* 'extreme' — батч 12, пауза 700 мс (риск bad_hash)
*
* Версия: 7.0
*/
(async function VKMusicCleaner() {
'use strict';
// ==================== КОНФИГУРАЦИЯ ====================
const CONFIG = {
speed: 'normal', // 'safe' | 'normal' | 'turbo' | 'extreme'
verbose: false, // true — логировать каждый батч; false — каждые 50 треков
protectUnload: true, // предупреждать при закрытии вкладки
hoverDelay: 30, // пауза после hover для активации кнопки (мс)
scrollStep: 800, // шаг скролла (px)
waitForRemoveMs: 5000, // ожидание исчезновения трека (мс)
stuckThreshold: 2, // сколько попыток до skip
emptyPauseMs: 5000, // пауза, если долго нет кнопок
};
const PROFILES = {
safe: { batchSize: 3, batchPause: 2000, scrollPause: 600 },
normal: { batchSize: 5, batchPause: 1500, scrollPause: 500 },
turbo: { batchSize: 8, batchPause: 1000, scrollPause: 400 },
extreme: { batchSize: 12, batchPause: 700, scrollPause: 300 },
};
const P = PROFILES[CONFIG.speed] || PROFILES.normal;
// ==================== ХРАНИЛИЩЕ ====================
const K = {
deleted: 'vk_music_deleted',
running: 'vk_music_running',
paused: 'vk_music_paused',
lastRun: 'vk_music_last_run',
stats: 'vk_music_stats',
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const get = (k, d) => { const v = localStorage.getItem(k); return v === null ? d : v; };
const set = (k, v) => localStorage.setItem(k, v);
const getNum = (k, d) => parseInt(get(k, d) || '0', 10);
const isRunning = () => get(K.running, '0') !== '0';
const setRunning = (v) => set(K.running, v ? '1' : '0');
const isPaused = () => get(K.paused, '0') === '1';
const setPaused = (v) => set(K.paused, v ? '1' : '0');
// Защита от повторного запуска.
const lastRun = getNum(K.lastRun, 0);
if (isRunning() && Date.now() - lastRun < 30_000) {
console.log('%c[VKMusicCleaner] Уже запущен. Полная остановка: localStorage.setItem("' + K.running + '","0")', 'color:#ff9800;font-weight:bold');
return;
}
setRunning(true);
setPaused(false);
set(K.lastRun, String(Date.now()));
// ==================== СОСТОЯНИЕ ====================
let deleted = getNum(K.deleted, 0);
let sessionDeleted = 0;
let consecutiveEmpty = 0;
let consecutiveErrors = 0;
let consecutiveSuccess = 0;
let currentBatchSize = P.batchSize;
let currentBatchPause = P.batchPause;
let badHashDetected = false;
let stuckSet = new Set();
let startTime = Date.now();
let paused = false;
// Статистика для ETA.
const stats = JSON.parse(get(K.stats, '[]'));
function pushStat() {
stats.push({ t: Date.now(), n: deleted });
if (stats.length > 200) stats.shift();
set(K.stats, JSON.stringify(stats));
}
// ==================== ЛОГ ====================
function log(msg, color = '#4aa3ff', force = false) {
if (!CONFIG.verbose && !force && !msg.includes('bad_hash') && !msg.includes('Готово') && !msg.includes('Старт')) {
// Тихий режим: пишем только milestones.
if (sessionDeleted % 50 !== 0 && sessionDeleted !== 0) return;
}
const ts = new Date().toLocaleTimeString();
console.log(`%c[${ts}] [VKMC/${CONFIG.speed}] ${msg}`, `color:${color}`);
}
// ==================== UI ОВЕРЛЕЙ ====================
const overlay = document.createElement('div');
overlay.id = 'vkmc-overlay';
overlay.style.cssText = `
position: fixed; top: 16px; right: 16px; z-index: 2147483647;
background: linear-gradient(135deg, #1e293b, #0f172a);
color: #e2e8f0; padding: 14px 18px; border-radius: 12px;
font: 13px/1.5 ui-monospace, Menlo, Consolas, monospace;
box-shadow: 0 10px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08);
min-width: 240px; cursor: move; user-select: none;
backdrop-filter: blur(10px);
`;
overlay.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<div style="width:10px;height:10px;border-radius:50%;background:#22c55e;box-shadow:0 0 10px #22c55e;" id="vkmc-dot"></div>
<strong style="font-size:14px;">VK Music Cleaner</strong>
</div>
<div id="vkmc-status" style="color:#94a3b8;font-size:11px;margin-bottom:8px;">работает…</div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:4px 12px;font-size:12px;">
<span style="color:#64748b;">Удалено:</span><span id="vkmc-deleted" style="color:#22c55e;font-weight:bold;">0</span>
<span style="color:#64748b;">Сессия:</span><span id="vkmc-session">0</span>
<span style="color:#64748b;">Скорость:</span><span id="vkmc-speed">—</span>
<span style="color:#64748b;">ETA:</span><span id="vkmc-eta">—</span>
<span style="color:#64748b;">Батч:</span><span id="vkmc-batch">—</span>
</div>
<div style="display:flex;gap:6px;margin-top:10px;">
<button id="vkmc-pause" style="flex:1;padding:6px;border:0;border-radius:6px;background:#f59e0b;color:#000;font-weight:600;cursor:pointer;font-size:11px;">⏸ Пауза</button>
<button id="vkmc-stop" style="flex:1;padding:6px;border:0;border-radius:6px;background:#ef4444;color:#fff;font-weight:600;cursor:pointer;font-size:11px;">⏹ Стоп</button>
</div>
<div style="margin-top:8px;font-size:10px;color:#475569;">Ctrl+Shift+P — пауза · Ctrl+Shift+R — resume</div>
`;
document.documentElement.appendChild(overlay);
// Drag&drop оверлея.
let dragOff = null;
overlay.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
const r = overlay.getBoundingClientRect();
dragOff = { x: e.clientX - r.left, y: e.clientY - r.top };
overlay.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!dragOff) return;
overlay.style.left = (e.clientX - dragOff.x) + 'px';
overlay.style.top = (e.clientY - dragOff.y) + 'px';
overlay.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
dragOff = null;
overlay.style.cursor = 'move';
});
function updateUI() {
const dot = overlay.querySelector('#vkmc-dot');
const status = overlay.querySelector('#vkmc-status');
if (paused) {
dot.style.background = '#f59e0b';
dot.style.boxShadow = '0 0 10px #f59e0b';
status.textContent = 'пауза';
} else if (badHashDetected) {
dot.style.background = '#ef4444';
dot.style.boxShadow = '0 0 10px #ef4444';
status.textContent = 'замедление (bad_hash)';
} else {
dot.style.background = '#22c55e';
dot.style.boxShadow = '0 0 10px #22c55e';
status.textContent = 'работает…';
}
overlay.querySelector('#vkmc-deleted').textContent = deleted;
overlay.querySelector('#vkmc-session').textContent = sessionDeleted;
overlay.querySelector('#vkmc-batch').textContent = `${currentBatchSize} / ${currentBatchPause}мс`;
const elapsed = (Date.now() - startTime) / 1000;
const rate = sessionDeleted > 0 && elapsed > 0 ? (sessionDeleted / elapsed) : 0;
overlay.querySelector('#vkmc-speed').textContent = rate > 0 ? `${rate.toFixed(1)} тр/сек` : '—';
if (rate > 0 && consecutiveEmpty < 3) {
const remainingEstimate = Math.max(0, 100 - (sessionDeleted % 100));
const etaSec = remainingEstimate / rate;
const m = Math.floor(etaSec / 60);
const s = Math.round(etaSec % 60);
overlay.querySelector('#vkmc-eta').textContent = `~${m}м ${s}с до чекпойнта`;
} else if (consecutiveEmpty >= 3) {
overlay.querySelector('#vkmc-eta').textContent = 'финализация…';
} else {
overlay.querySelector('#vkmc-eta').textContent = '—';
}
}
overlay.querySelector('#vkmc-pause').addEventListener('click', () => {
paused = !paused;
setPaused(paused);
overlay.querySelector('#vkmc-pause').textContent = paused ? '▶ Продолжить' : '⏸ Пауза';
updateUI();
log(paused ? 'Пауза' : 'Продолжаю', '#f59e0b', true);
});
overlay.querySelector('#vkmc-stop').addEventListener('click', () => {
setRunning(false);
log('Остановка пользователем', '#ef4444', true);
});
// Горячие клавиши.
document.addEventListener('keydown', (e) => {
if (!e.ctrlKey || !e.shiftKey) return;
if (e.key === 'P' || e.key === 'p') {
e.preventDefault();
paused = !paused;
setPaused(paused);
overlay.querySelector('#vkmc-pause').textContent = paused ? '▶ Продолжить' : '⏸ Пауза';
updateUI();
log(paused ? 'Пауза (hotkey)' : 'Продолжаю (hotkey)', '#f59e0b', true);
} else if (e.key === 'R' || e.key === 'r') {
e.preventDefault();
paused = false;
setPaused(false);
overlay.querySelector('#vkmc-pause').textContent = '⏸ Пауза';
updateUI();
log('Resume (hotkey)', '#22c55e', true);
} else if (e.key === 'S' || e.key === 's') {
e.preventDefault();
setRunning(false);
log('Stop (hotkey)', '#ef4444', true);
}
});
// Защита от закрытия вкладки.
if (CONFIG.protectUnload) {
window.addEventListener('beforeunload', (e) => {
if (isRunning()) {
e.preventDefault();
e.returnValue = 'VK Music Cleaner ещё работает. Точно закрыть?';
return e.returnValue;
}
});
}
// ==================== DOM УТИЛИТЫ ====================
function fireHover(el) {
const opts = { bubbles: true, cancelable: true, view: window, button: 0 };
el.dispatchEvent(new MouseEvent('pointerenter', opts));
el.dispatchEvent(new MouseEvent('pointerover', opts));
el.dispatchEvent(new MouseEvent('mouseenter', opts));
el.dispatchEvent(new MouseEvent('mouseover', opts));
el.dispatchEvent(new MouseEvent('mousemove', opts));
}
// Найти ключ React Props на элементе.
function getReactProps(el) {
const key = Object.keys(el).find((k) =>
k.startsWith('__reactProps$') || k.startsWith('__reactEventHandlers$')
);
return key ? el[key] : null;
}
// «Умный» клик: сначала пробуем React onClick напрямую, потом фоллбэк на события.
function smartClick(el) {
const props = getReactProps(el);
if (props && typeof props.onClick === 'function') {
try {
props.onClick({
preventDefault() {}, stopPropagation() {}, stopImmediatePropagation() {},
target: el, currentTarget: el, type: 'click', nativeEvent: {},
isDefaultPrevented: () => false, isPropagationStopped: () => false,
});
return true;
} catch (_) {}
}
// Фоллбэк.
const opts = { bubbles: true, cancelable: true, view: window, button: 0 };
el.dispatchEvent(new MouseEvent('mouseover', opts));
el.dispatchEvent(new MouseEvent('mousemove', opts));
el.dispatchEvent(new MouseEvent('mousedown', opts));
el.dispatchEvent(new MouseEvent('mouseup', opts));
el.dispatchEvent(new MouseEvent('click', opts));
try { el.click(); } catch (_) {}
return false;
}
// Поиск скролл-контейнера внутри секции треков.
function findScrollContainer() {
const roots = [
...document.querySelectorAll('[data-testid="AudioCatalog_SectionTracks"]'),
...document.querySelectorAll('[data-testid="AudioCatalog_BlockMusicAudiosList"]'),
...document.querySelectorAll('.vkuiList__host'),
...document.querySelectorAll('[class*="AudioList"]'),
];
for (const root of roots) {
let node = root;
for (let i = 0; i < 10 && node; i++) {
const style = getComputedStyle(node);
if (
(style.overflowY === 'auto' || style.overflowY === 'scroll') &&
node.scrollHeight > node.clientHeight + 50
) {
return node;
}
node = node.parentElement;
}
}
return document.scrollingElement || document.documentElement;
}
function scrollDownStep(container) {
const before = container.scrollTop;
container.scrollTop = before + CONFIG.scrollStep;
window.scrollBy(0, CONFIG.scrollStep);
return container.scrollTop !== before;
}
function scrollToEl(container, el) {
try { el.scrollIntoView({ block: 'center', behavior: 'instant' }); }
catch (_) { el.scrollIntoView(); }
}
function getTrackRows() {
return [...document.querySelectorAll('[data-testid="MusicTrackRow"]')];
}
function findRemoveButtonInRow(row) {
let btn = row.querySelector(
'[data-testid="MusicAudio_ToggleOwning"][data-testactive="true"],' +
'[data-testid="MusicAudio_ToggleOwning"][aria-pressed="true"]'
);
if (btn) return btn;
const all = row.querySelectorAll('[data-testid="MusicAudio_ToggleOwning"]');
for (const b of all) {
const label = (b.textContent || b.getAttribute('aria-label') || '').toLowerCase();
if (label.includes('удалить')) return b;
}
for (const b of all) {
const al = (b.getAttribute('aria-label') || '').toLowerCase();
if (al.includes('удалить')) return b;
}
return null;
}
async function collectVisibleButtons() {
const rows = getTrackRows();
const buttons = [];
for (const row of rows) {
// Уникальный идентификатор строки для skip-list.
const audioId = row.closest('[data-audio-id]')?.getAttribute('data-audio-id') || '';
if (stuckSet.has(audioId)) continue;
fireHover(row);
await sleep(CONFIG.hoverDelay);
const btn = findRemoveButtonInRow(row);
if (btn && btn.isConnected) buttons.push({ row, btn, audioId });
}
return buttons;
}
async function waitBatchRemoved(items) {
const start = Date.now();
const stuck = [];
while (Date.now() - start < CONFIG.waitForRemoveMs) {
let remaining = 0;
for (const item of items) {
const { row, btn, audioId } = item;
if (!row.isConnected) continue;
if (!btn.isConnected) continue;
const a = btn.getAttribute('data-testactive');
const p = btn.getAttribute('aria-pressed');
if (a === 'false' || p === 'false') continue;
remaining++;
}
if (remaining === 0) return { ok: true, stuck };
await sleep(150);
}
// Финальный проход: кто остался — те «застряли».
for (const item of items) {
const { row, btn, audioId } = item;
if (audioId && row.isConnected && btn.isConnected) {
const a = btn.getAttribute('data-testactive');
const p = btn.getAttribute('aria-pressed');
if (a !== 'false' && p !== 'false') {
stuck.push(item);
}
}
}
return { ok: false, stuck };
}
// ==================== ПЕРЕХВАТ ОШИБОК ВК ====================
const origFetch = window.fetch;
window.fetch = async function (...args) {
const res = await origFetch.apply(this, args);
try {
const clone = res.clone();
const text = await clone.text();
if (text.includes('bad_hash')) badHashDetected = true;
} catch (_) {}
return res;
};
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this._url = url;
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
this.addEventListener('load', function () {
try {
if (this.responseText && this.responseText.includes('bad_hash')) {
badHashDetected = true;
}
} catch (_) {}
});
return origSend.apply(this, arguments);
};
// ==================== ГЛАВНЫЙ ЦИКЛ ====================
log(`Старт. Уже удалено ранее: ${deleted}. Батч: ${currentBatchSize}/${currentBatchPause}мс`, '#4aa3ff', true);
log(`Скорость: ${CONFIG.speed}. verbose: ${CONFIG.verbose}. Защита вкладки: ${CONFIG.protectUnload}`, '#94a3b8', true);
const container = findScrollContainer();
log(`Скролл-контейнер: <${container.tagName.toLowerCase()} class="${(container.className || '').slice(0, 50)}">`, '#9c27b0', true);
const uiInterval = setInterval(updateUI, 500);
while (isRunning()) {
set(K.lastRun, String(Date.now()));
// Пауза.
while (paused && isRunning()) {
await sleep(300);
}
if (!isRunning()) break;
// bad_hash → плавно замедляемся.
if (badHashDetected) {
consecutiveErrors++;
consecutiveSuccess = 0;
badHashDetected = false;
currentBatchSize = Math.max(1, currentBatchSize - 1);
currentBatchPause = Math.min(8000, currentBatchPause + 500);
const pauseMs = 3000 + consecutiveErrors * 1500;
log(`⚠️ bad_hash (#${consecutiveErrors}). Батч=${currentBatchSize}, пауза=${currentBatchPause}мс, ждём ${pauseMs}мс`, '#ef4444', true);
updateUI();
await sleep(pauseMs);
continue;
}
// Плавное восстановление скорости.
if (consecutiveErrors > 0 || currentBatchSize < P.batchSize) {
consecutiveSuccess++;
if (consecutiveSuccess >= 5) {
consecutiveSuccess = 0;
if (consecutiveErrors > 0) consecutiveErrors--;
if (currentBatchSize < P.batchSize) currentBatchSize++;
if (currentBatchPause > P.batchPause) currentBatchPause = Math.max(P.batchPause, currentBatchPause - 200);
log(`✓ Восстановление: батч=${currentBatchSize}, пауза=${currentBatchPause}мс`, '#22c55e');
}
}
const items = (await collectVisibleButtons()).filter((i) => !stuckSet.has(i.audioId));
if (items.length === 0) {
const moved = scrollDownStep(container);
await sleep(P.scrollPause);
if (!moved) {
consecutiveEmpty++;
log(`Низ списка (попытка ${consecutiveEmpty})`, '#f59e0b');
if (consecutiveEmpty % 3 === 0) {
container.scrollTop = 0;
window.scrollTo(0, 0);
await sleep(800);
}
if (consecutiveEmpty > 10) {
log(`Долго пусто. Пауза ${CONFIG.emptyPauseMs}мс`, '#ef4444');
await sleep(CONFIG.emptyPauseMs);
}
if (consecutiveEmpty > 20) {
log(`Похоже, всё удалено. Останавливаюсь.`, '#22c55e', true);
break;
}
}
updateUI();
continue;
}
consecutiveEmpty = 0;
log(`Найдено ${items.length} кнопок. Батч=${currentBatchSize}`, '#4aa3ff');
for (let i = 0; i < items.length; i += currentBatchSize) {
if (!isRunning()) break;
while (paused && isRunning()) await sleep(300);
if (badHashDetected) break;
const batch = items.slice(i, i + currentBatchSize);
if (batch[0] && batch[0].row.isConnected) {
scrollToEl(container, batch[0].row);
await sleep(60);
}
let clickedInBatch = 0;
for (const { row, btn } of batch) {
if (!row.isConnected || !btn.isConnected) continue;
const a = btn.getAttribute('data-testactive');
const p = btn.getAttribute('aria-pressed');
if (a === 'false' || p === 'false') continue;
fireHover(row);
smartClick(btn);
clickedInBatch++;
}
if (clickedInBatch > 0) {
const { ok, stuck } = await waitBatchRemoved(batch);
deleted += clickedInBatch - stuck.length;
sessionDeleted += clickedInBatch - stuck.length;
set(K.deleted, String(deleted));
pushStat();
// Добавляем «застрявших» в skip-list.
for (const s of stuck) {
if (s.audioId) {
stuckSet.add(s.audioId);
log(`⏭ Skip ${s.audioId} (застрял)`, '#f59e0b');
}
}
if (ok) {
consecutiveSuccess++;
log(`+${clickedInBatch} → всего ${deleted} (сессия ${sessionDeleted}) ✓`, '#22c55e');
} else {
log(`+${clickedInBatch - stuck.length} → всего ${deleted} (${stuck.length} застряли)`, '#f59e0b');
}
updateUI();
}
await sleep(currentBatchPause);
}
scrollDownStep(container);
await sleep(P.scrollPause);
updateUI();
}
// ==================== ФИНАЛИЗАЦИЯ ====================
clearInterval(uiInterval);
window.fetch = origFetch;
XMLHttpRequest.prototype.open = origOpen;
XMLHttpRequest.prototype.send = origSend;
setRunning(false);
setPaused(false);
if (overlay.parentNode) overlay.remove();
const elapsedSec = Math.round((Date.now() - startTime) / 1000);
const m = Math.floor(elapsedSec / 60);
const s = elapsedSec % 60;
log(`=== Готово! Удалено: ${deleted} (за сессию: ${sessionDeleted} за ${m}м ${s}с) ===`, '#22c55e', true);
log(`Skip-list: ${stuckSet.size} треков не поддались.`, '#94a3b8', true);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment