English · Русский
Расширение Chrome для скачивания видео с Rutube, VK Video и других сайтов. Значок в панели → список найденного на странице → выбор качества → «Скачать». Ничего сверх этого.
| Сайт | Как ловится | Что это даёт |
|---|---|---|
| Rutube | свой адаптер | название, обложка, длительность и все качества видны сразу при открытии страницы, не дожидаясь Play. Плейлисты качаются пачкой |
| VK Video | свой адаптер | то же самое, и вдобавок VK отдаёт готовые файлы по качествам — они качаются напрямую, без пересборки |
| Другие сайты | универсальный перехват | если браузер играет видео потоком (HLS) или отдаёт файлом, расширение обычно сможет его скачать |
Проще говоря: у Rutube и VK всё разобрано до мелочей, остальные сайты работают по общему правилу — что плеер скачал, то и мы можем сохранить.
chrome://extensions→ включить «Режим разработчика»- «Загрузить распакованное расширение» → указать эту папку
Сборка не нужна: ванильный 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 расширение не трогает по построению.