Skip to content

Repository files navigation

VideoGrabber

English · Русский

Расширение Chrome для скачивания видео с Rutube, VK Video и других сайтов. Значок в панели → список найденного на странице → выбор качества → «Скачать». Ничего сверх этого.

Какие сайты

Сайт Как ловится Что это даёт
Rutube свой адаптер название, обложка, длительность и все качества видны сразу при открытии страницы, не дожидаясь Play. Плейлисты качаются пачкой
VK Video свой адаптер то же самое, и вдобавок VK отдаёт готовые файлы по качествам — они качаются напрямую, без пересборки
Другие сайты универсальный перехват если браузер играет видео потоком (HLS) или отдаёт файлом, расширение обычно сможет его скачать

Проще говоря: у Rutube и VK всё разобрано до мелочей, остальные сайты работают по общему правилу — что плеер скачал, то и мы можем сохранить.

Установка

  1. chrome://extensions → включить «Режим разработчика»
  2. «Загрузить распакованное расширение» → указать эту папку

Сборка не нужна: ванильный JS и ES-модули, mux.js лежит готовым файлом в offscreen/vendor/.

Как это устроено

Две линии обнаружения. Адаптеры по адресу страницы сразу знают название, длительность, обложку, список качеств и наличие DRM — не дожидаясь нажатия Play: adapters/rutube.js дёргает api/play/options, adapters/vk.js — страницу встраивания video_ext.php. Параллельно background/detect.js слушает трафик вкладки и ловит .m3u8 и крупные video/* на сайтах, для которых адаптера нет. Когда адаптер отработал, находки сниффера по этой вкладке не показываются, чтобы не засорять список.

На сайте с адаптером сниффер молчит совсем — даже если адаптер не справился. Иначе в список попадает то, что плеер тянет для себя: у VK это webm-фрагменты DASH, выглядящие готовым файлом на пару мегабайт, а на деле кусок потока, который не открывается. Сниффер к тому же успевает раньше адаптера, когда страница уже играет. Провал адаптера при этом виден в списке с причиной, а не молчит: иначе не отличить «здесь нет видео» от «разбор сломался».

Кто качает — тот и показывает прогресс. У HLS сегменты тянет расширение, поэтому полоса идёт по ним. Прогрессивный файл качает сам Chrome, и сколько получено, знает только он — этап называется downloading, а числа берутся из downloads.search в момент открытия попапа, без фоновых таймеров. Отдельный этап нужен и по второй причине: на saving кнопка отмены отключена (файл уже собран, отменять нечего), а получасовую загрузку Chrome остановить надо уметь.

У VK файлы берутся напрямую. VK отдаёт готовые mp4_144 … mp4_1080 рядом с HLS, поэтому его видео качается как обычный файл — без сегментов, ремукса и правки заголовка. HLS остаётся запасным путём: у части роликов прямых файлов нет.

Две ловушки той страницы, обе стоили отладки. Она отдаётся в windows-1251, и на UTF-8 кириллица в названии превращается в мусор — кодировка читается из заголовка ответа. А ключ "title" встречается в ней несколько раз: он есть и у дорожки субтитров, причём раньше настоящего. Поэтому объект видео находится по блоку files и разбирается целиком, а не выхватывается регуляркой.

Реклама отсеивается по происхождению, а не по формату. Рекламный ролик — такой же mp4, как нужный, и по типу файла их не различить: фильтровать «видео или нет» бессмысленно. Различимо, кто его отдаёт, поэтому lib/adhosts.js отсеивает запросы с доменов рекламных сетей и туда же смотрит по инициатору запроса. Правило ошибается в сторону сохранения: всё, что не с рекламного хоста, остаётся в списке. Список неизбежно неполный — он работает в паре со следующим правилом.

Реклама из чужих iframe игнорируется. Видеореклама живёт в стороннем iframe, и без этого правила страница с одними PDF набирала восемь «видео» с рекламного плеера — с одинаковыми подписями, потому что имя находки берётся из заголовка вкладки. Теперь берётся главный фрейм и врезки того же сайта (домен второго уровня совпадает), а чужие фреймы отбрасываются: сторонний встроенный плеер при этом теряется — размен сознательный, рекламы кратно больше. Заодно в подписи выводится имя файла или хост, чтобы несколько находок с одной страницы можно было различить.

Плейлист качается пачкой. Если в адресе есть ?playlist=<id>, рядом с роликом появляется карточка плейлиста и кнопка «Скачать все»: список берётся из api/playlist/custom/<id>/videos/ постранично (по 20, потолок — 200 роликов), удалённые, скрытые, платные и эфиры отсеиваются сразу. Каждый ролик становится обычным заданием в той же очереди, поэтому пачка на два десятка не выедает ни канал, ни память, а докачка и повтор работают для неё как для одиночной.

Качество для пачки задаётся высотой, а не ключом потока: у роликов наборы качеств разные, и общего ключа не существует — качалка берёт ближайшее доступное у каждого. Отсюда же {q} в шаблоне имени: фактическое качество известно только после разбора мастер-плейлиста, и подставляется оно в момент сохранения. Иначе файл, где нашлось только 480p, назывался бы «(720p)».

Поток останавливается там, где запускался. У начатой пачки кнопка «Скачать все» на карточке плейлиста превращается в «Пауза» / «Продолжить», под ней появляется «Отменить все», и там же видно, сколько роликов готово. Ходить за этим во вкладку «Загрузки» не нужно, хотя те же кнопки есть и там — уже для всей очереди сразу.

Пачка управляется отдельно от очереди: рядом может идти обычная загрузка, и глушить её заодно неправильно. Поэтому у задания есть метка пачки, а pauseBatch / resumeBatch / cancelBatch работают только по ней. Пауза откладывает и то, что уже идёт: HLS прерывается в качалке, прогрессивные приостанавливает сам Chrome. Без этого пачка неуправляема — пока отменяешь одно задание, успевает стартовать следующее.

Приостановленное — незаконченная работа: его не вытесняет история, не трогает «Очистить завершённые» и не отменяет перезапуск браузера. Стоявшее в очереди при перезапуске тоже уходит в паузу, а не в ошибку: оно ещё не начиналось, терять нечего. Само после рестарта ничего не стартует — только по команде.

Загрузки не привязаны к вкладке. Реестр находок живёт по вкладкам и чистится при уходе со страницы, а задания — нет: в background/jobs.js каждое задание самодостаточно (в request лежит всё для перезапуска), хранится в chrome.storage.local и переживает закрытие вкладки и перезапуск браузера. Очередь держит не больше двух одновременных загрузок. Отличить перезапуск браузера от обычного усыпления воркера помогает флаг в chrome.storage.session: он живёт ровно сессию, поэтому «повисшие» задания помечаются прерванными только после реального рестарта браузера, а не каждый раз, когда Chrome усыпил воркер.

Загрузка живёт в offscreen-документе. Service worker в MV3 засыпает в любой момент, поэтому скачивание вынесено на отдельную страницу (offscreen/downloader.js), которая держится до конца работы. Закрытие попапа и усыпление воркера загрузку не прерывают.

Сохранение разнесено на два контекста. В offscreen-документе из chrome.* доступен только runtime — ни downloads, ни storage там нет. Поэтому файл собирается в offscreen (только там есть URL.createObjectURL), а сохраняет его воркер: ему передаётся blob-ссылка. Offscreen нельзя закрывать, пока downloads.onChanged не сообщит complete — иначе ссылка отзовётся на середине записи и файл оборвётся.

Длительность в заголовке проставляется вручную. mux.js отдаёт фрагментированный MP4 и пишет в mvhd/mdhd значение «неизвестно» (0xFFFFFFFF), а в tkhd — ноль. Браузеры сами сканируют фрагменты и считают реальную длину, а Windows-плеер и Clipchamp верят заголовку: показывают 13:15:21 (0xFFFFFFFF / 90000) и держат видеодорожку нулевой длины — картинка стоит на первом кадре, звук идёт. Длительность известна заранее из суммы #EXTINF, поэтому lib/mp4.js проставляет её в init-сегмент. Там же keepOriginalTimestamps: false — таймлайн файла начинается с нуля, а не с сырого PTS транспортного потока.

Докачка — только для длинных файлов. Начиная с 10 минут готовые фрагменты складываются в IndexedDB (offscreen/store.js) по одному куску на пачку сегментов, вместе с отметкой, до какого места дошли — одной транзакцией, иначе после сбоя отметка обещала бы больше, чем лежит на диске. Обрыв переводит задание в «прервано» с кнопкой «Продолжить», и оно докачивается с места остановки. Короткий ролик проще перекачать заново, чем платить за него диском.

Тонкость, на которой это легко сломать: при докачке транскодер новый, и его таймлайн начинается с нуля. Приклеенный к уже скачанной половине, он даёт файл вдвое короче реального — проверено на контрольном замере. Поэтому перед продолжением вызывается setBaseMediaDecodeTime с суммой #EXTINF до места обрыва; с ним склейка выходит байт в байт как цельная загрузка. По той же причине ссылка на плейлист обновляется перед каждым стартом: за часы между попытками подписи CDN протухают, и мастер идёт последним запасным вариантом.

Сборка файла. Сегменты качаются пачками по 6, с тремя попытками и запасными CDN-зеркалами того же качества. TS-сегменты проходят через mux.js (ремукс без перекодирования), fMP4/CMAF склеиваются как есть вместе с init-сегментом из #EXT-X-MAP. Каждые ~50 сегментов накопленные куски схлопываются в Blob — Chrome держит его на диске, поэтому память не растёт вместе с длиной видео.

Значок и зелёная кнопка. Значок цветной, когда на вкладке есть что скачать, и серый, когда нет: в манифесте по умолчанию стоит серый набор, цветной ставится через chrome.action.setIcon по вкладке из background/registry.js. Кнопка «Скачать» зеленеет, если это видео уже лежит на диске — причём перед этим downloads.search проверяет, что файл не удалён: запись в истории живёт дольше самого файла, и зелёная кнопка на удалённое видео вводила бы в заблуждение.

Настройки (шестерёнка в попапе) — подпапка в «Загрузках», системный диалог сохранения, качество по умолчанию и качество в имени файла. Собирает имя чистая функция buildFilename в lib/settings.js, поэтому её поведение закрыто тестами. Там же вырезаются .., ведущие слэши и буква диска: Chrome отвергает такой filename целиком, и лучше поправить путь заранее, чем ронять загрузку.

manifest.json
background/   service worker, реестр находок, сниффер трафика, очередь заданий
adapters/     разбор конкретных сайтов (Rutube, VK Video)
              остальные ловятся общим сниффером, без адаптера
lib/          парсер m3u8, заголовок mp4, сетевые обёртки, имена, настройки
offscreen/    качалка, ремукс, докачка, сборка файла
popup/        интерфейс
options/      страница настроек

Что поддерживается

Rutube да, со своим адаптером
VK Video да, своим адаптером, прямыми файлами
HLS, TS-сегменты да, ремукс в MP4
HLS, fMP4/CMAF да, склейка без ремукса
HLS с AES-128 да, ключ берётся так же, как его берёт плеер
Прогрессивный .mp4 да, качается напрямую
Плейлисты Rutube да, пачкой через общую очередь
DASH (.mpd) нет
Прямые эфиры нет, определяются по отсутствию #EXT-X-ENDLIST
DRM (Widevine и подобное) нет и не планируется
Несколько аудиодорожек берётся основная

Добавить новый сайт

Один файл в adapters/ с методами match(url), probe(url, id, tabId) и, если ссылки на плейлист протухают, refresh(entry). Дальше — строка в adapters/index.js. Всё остальное ядро уже умеет.

Проверено

node tests/run.mjs          # все наборы
node tests/run.mjs jobs     # только один

Внешних зависимостей нет, нужен только node. Все наборы работают на настоящих модулях проекта, а не на копиях.

набор проверок что закрывает
test_parser 43 плейлисты Rutube, выбор качества, разбор пачки
test_vk 28 ссылки VK, разбор страницы плеера, качества
test_mp4 11 правка длительности в заголовке, защитные пути
test_settings 31 подпапка, имя файла, шаблон качества
test_registry 17 реестр находок, цвет значка, предел на вкладку
test_detect 47 доверие к фрейму, отсев рекламы, сайты с адаптером
test_version 10 версия, описание, состав адаптеров
test_jobs 75 очередь, пауза пачки, перезапуски, докачка, прогресс

test_parser ходит в сеть за живыми плейлистами Rutube и разбирает настоящий плейлист через сам адаптер — без интернета он упадёт, это ожидаемо. Остальные автономны.

test_jobs, test_registry и test_detect работают на подставном chrome.*: очередь, перезапуск браузера, докачку и цвет значка иначе пришлось бы ловить руками. Пайплайн ремукса проверялся на реальных сегментах в браузере — на выходе ftyp/isom/avc1, две дорожки (avc1 + mp4a), длительность сходится с суммой #EXTINF до сотых, перемотка и воспроизведение работают.

Иконки собираются из tools/make_icons.py (py -3 tools/make_icons.py) — чистый stdlib, без Pillow.

Версии

Что в какой версии — в CHANGELOG.md. Версия в manifest.json это то, что Chrome показывает в списке расширений; она обязана совпадать с верхней записью журнала, и это проверяет test_version — иначе версия отстаёт от кода незаметно, как случилось с 1.0.0.

Порядок выпуска: правка в CHANGELOG.md → та же версия в manifest.json → node tests/run.mjs → коммит → git tag v<версия> → git push --tags.

Лицензия

MIT — берите, меняйте, используйте где угодно, достаточно сохранить упоминание автора. Текст в LICENSE.

Сторонние компоненты

offscreen/vendor/mux.min.js — библиотека mux.js версии 7.1.0 под лицензией Apache 2.0. Она лежит в репозитории готовым файлом, потому что сборки у проекта нет; её лицензионная шапка сохранена внутри файла.

Оговорка

Инструмент общего назначения. Скачивание чужого контента может нарушать пользовательское соглашение сайта и авторские права — это на усмотрение пользователя. Защищённое DRM расширение не трогает по построению.

About

Расширение Chrome для скачивания видео с Rutube, VK Video и других сайтов: выбор качества, очередь загрузок, пауза, докачка после обрыва, пакетная закачка плейлистов. Без сборки и зависимостей.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages