Pull to refresh

Борьба с современным Web. Создаём расширение для скачивания видео из VK

Level of difficultyMedium
Reading time5 min
Views11K

Всем привет! Пишу впервые, хоть и читаю Хабр с 2012 года. Буду рад любой обратной связи. Поехали.

Предыстория

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

  • Постоянные ошибки

  • Низкая частота кадров при качестве 720p и выше

  • Графические артефакты при качестве 480p и ниже

Я решил воспользоваться безотказным методом — скачивать видео и смотреть их локально. Однако, найти ссылки на mp4 файлы в исходниках desktop-версии VK мне не удалось. Несколько лет назад — удавалось. Видимо теперь всё работает через более хитрый стриминг. Возможно по этой же причине более не работают сторонние сервисы для скачивания видео.

Потратив немного времени, я выяснил, что ссылки на файлы можно выудить из мобильной версии VK. Каждый раз вытаскивать их руками, путаясь в качестве видео, оказалось неудобно. Так и зародилась идея браузерного расширения: хотелось бы, чтобы при переходе на страницу с любым видео, под плеером отображались ссылки для скачивания его в любом из доступных качеств.

Реализация

Для начала, создадим manifest.json — файл, содержащий информацию о расширении. Помимо обязательных полей, добавим в него поле content_scripts, содержащее следующее правило: при переходе на любую страницу на домене m.vk.com и полной загрузке DOM исполнить код, находящийся в файле content-script.js. Такой код называется встраиваемым скриптом. Манифест может содержать огромное количество разнообразных полей. Подробная документация по этому поводу — здесь.

// manifest.json

{
  "manifest_version": 3,
  "name": "VK Video Downloader",
  "version": "1.0.0",
  "action": {},
  "content_scripts": [
    {
      "js": ["content-script.js"],
      "matches": ["https://m.vk.com/*"],
      "run_at": "document_idle"
    }
  ]
}
// content-script.js

alert('Hello, World!');
Загружаем расширение в браузер. Переходим по нужному URL. Довольствуемся результатом.
Загружаем расширение в браузер. Переходим по нужному URL. Довольствуемся результатом.

Перейдя на страницу любого видео и взглянув на HTML-дерево, мы увидим примерно следующее:

<div class="VideoPage__video">
  <video class="vv_inline_video">
    <source src="https://vkvd97.mycdn.me/video.m3u8?..." type="application/vnd.apple.mpegurl">
    <source src="https://vkvd97.mycdn.me/...type=1&amp;..." type="video/mp4">
    <source src="https://vkvd97.mycdn.me/...type=0&amp;..." type="video/mp4">
    <source src="https://vkvd97.mycdn.me/...type=4&amp;..." type="video/mp4">
  </video>
</div>

Нас интересуют значения аттрибутов src тегов <source>, отвечающих за mp4 файлы. И я не просто так отметил url-параметр type в каждом из них. Именно он ответственен за качество видео. Единожды скачав все возможные вариации одного видео, я вывел таблицу соответствия:

Значение параметра type

Качество видео

0

240p

1

360p

2

480p

3

720p

4

144p

5

1080p

6

1440p

7

2160p

Да, 144p выбивается из общей логики и имеет тип 4. Возможно отголоски какого-то легаси, а возможно я просто чего-то не знаю и кто-нибудь просветит меня в комментариях :)

Реализуем функцию, собирающую и возвращающую нужные нам данные в удобном для дальнейшей обработки формате:

// content-script.js

function getVideoSources() {
  const sourceTags = document.querySelectorAll(
    'video source[type="video/mp4"]'
  );
  let videoSources = {};

  for (const tag of sourceTags) {
    if (tag.src.includes('&type=4')) {
      videoSources['144p'] = tag.src;
    } else if (tag.src.includes('&type=0')) {
      videoSources['240p'] = tag.src;
    } else if (tag.src.includes('&type=1')) {
      videoSources['360p'] = tag.src;
    } else if (tag.src.includes('&type=2')) {
      videoSources['480p'] = tag.src;
    } else if (tag.src.includes('&type=3')) {
      videoSources['720p'] = tag.src;
    } else if (tag.src.includes('&type=5')) {
      videoSources['1080p'] = tag.src;
    } else if (tag.src.includes('&type=6')) {
      videoSources['1440p'] = tag.src;
    } else if (tag.src.includes('&type=7')) {
      videoSources['2160p'] = tag.src;
    }
  }

  return videoSources;
}

console.log(getVideoSources());

Теперь, перезагрузив расширение и открыв страницу любого видео, мы увидим в консоли браузера нечто подобное (количество полей будет варьироваться в зависимости от максимального качества видео):

{
  "144p": "https://vkvd97.mycdn.me/...type=4&amp;...",
  "240p": "https://vkvd97.mycdn.me/...type=0&amp;...",
  "360p": "https://vkvd97.mycdn.me/...type=1&amp;..."
}

Казалось бы, задача практически решена, однако именно на этом этапе разработки, я заметил нюанс, заставивший меня поломать голову.

При вводе URL страницы с видео в адресную строку и нажатии Enter скрипт срабатывал. При переходе на страницу с другой страницы VK — не срабатывал, однако стоило её перезагрузить — он всё же срабатывал.

Насколько мне удалось выяснить, дело в том, что при навигации по VK далеко не всегда происходит полноценный переход на новую страницу. Во многих случаях просто перестраивается DOM, что не является условием срабатывания встраиваемого скрипта.

Придётся действовать иначе. Будем отслеживать изменения DOM в реальном времени и извлекать данные лишь в том случае, если находимся на странице видео (путь начинается с /video-) и заметили появление в документе контейнера видеоплеера (имеет класс VideoPage__video). Поможет нам в этом такая замечательная вещь, как MutationObserver. Также, стоит учитывать то, что отрисоваться контейнер может далеко не сразу. Поэтому, при переходе на страницу с видео, будем искать его каждые 100 мс, пока не найдём.

// content-script.js

let lastUrl = location.href;

new MutationObserver(() => {
  if (location.href !== lastUrl) {
    lastUrl = location.href;
  }
  if (location.pathname.includes('/video-')) {
    const checker = setInterval(() => {
      if (
        document.querySelector('div.VideoPage__video')
      ) {
        clearInterval(checker);
        getVideoSources();
      }
    }, 100);
  }
}).observe(document, { subtree: true, childList: true });

function getVideoSources() {...}

Замечательно. Теперь, стоит плееру появиться на странице (неважно каким образом вы на эту страницу попали), в консоль выводятся нужные нам данные. Давайте реализуем функционал для отображения их непосредственно на странице, немного подогнав под вёрстку VK:

// content-script.js

function createDownloadPanel(videoSources) {
  const label = document.createElement('span');
  label.innerText = 'Скачать:';
  label.style.marginRight = '2px';

  const panel = document.createElement('div');
  panel.id = 'vkVideoDownloaderPanel';
  panel.style.margin = '8px 12px';
  panel.appendChild(label);

  for (const [quality, url] of Object.entries(videoSources)) {
    const aTag = document.createElement('a');
    aTag.href = url;
    aTag.innerText = quality;
    aTag.style.margin = '0 2px';
    panel.appendChild(aTag);
  }

  return panel;
}

function showPanel(panel) {
  document.querySelector('div.VideoPage__video').after(panel);
}
Совместив весь код выше получаем такой вот результат
Совместив весь код выше получаем такой вот результат

Чуть позже я немного доработал расширение:

  • Добавил обработку видео, встроенных со сторонних сайтов (в контейнере плеера в таком случае находится не <video>, а <iframe>)

  • Исправил баги, из-за которых, функция отвечающая за отрисовку панели для скачивания n-ое количество раз отрабатывала вхолостую, ничего не отрисовывая

  • Добавил иконки

  • Дополнил manifest.json

Взглянуть на финальную версию и воспользоваться ей можно перейдя на мой GitHub. Уже получил несколько благодарностей на почту за публикацию этого расширения. Может будет полезно и вам.

Спасибо, что дочитали до конца.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+28
Comments25

Articles