Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
12 KiB
Outlier Saver — дизайн расширения (версия 2: данные VidIQ)
Дата: 2026-10-25. Версия 1 (YouTube Data API и свой расчёт score) снята: API недоступен в регионе пользователя. Предыдущая версия лежит в истории git (коммит 05d8bff).
Цель
Расширение для Chrome (Manifest V3) для личного использования. Пока пользователь смотрит YouTube с установленным VidIQ, оно читает карточки видео, берёт у VidIQ outlier score и число подписчиков и сохраняет видео с высоким score локально. Позже данные выгружаются в JSON или CSV.
Принятые решения
| Вопрос | Решение |
|---|---|
| Источник данных | Только DOM страницы YouTube. Никаких запросов к сети, API-ключей и host-разрешений |
| Outlier score | Берётся у VidIQ, своя формула удалена |
| Подписчики | Берутся у VidIQ из карточки (округлены, как на экране) |
| Просмотры | Берутся из разметки YouTube (округлены, как на экране) |
| Порог | Сохраняем при outlierScore ≥ threshold и views ≥ minViews (по умолчанию 5 и 10 000, меняются в настройках) |
| Хранение | IndexedDB в браузере, экспорт в JSON и CSV из popup |
| Страницы | Главная, страница просмотра (боковые карточки), поиск, страница канала |
| Языки интерфейса | Русский и английский |
| Сборка | Нет. Чистый JS, ES-модули, загрузка как «распакованное расширение» |
Откуда берётся каждое поле
Опорные точки взяты из реальной разметки карточки с главной (образец лежит в test/fixtures/home-card.html). Привязка идёт к атрибутам и ссылкам VidIQ, а не к его сгенерированным классам вида vidiq-c-bXbWpx.
| Поле | Источник |
|---|---|
videoId, isShort |
Ссылка /watch?v=<id> или /shorts/<id> внутри карточки |
title |
h3[title], иначе #video-title, иначе .ytLockupMetadataViewModelTitle |
thumbnailUrl |
img с src на https://i.ytimg.com/vi/…, иначе https://i.ytimg.com/vi/<id>/mqdefault.jpg |
channelTitle, channelUrl |
Первая ссылка /@…, /channel/…, /c/…, /user/… с текстом, вне блоков VidIQ. Если ссылки нет (боковая панель), то текст канала из строки метаданных. На странице канала — из URL страницы и заголовка вкладки |
views |
Первый элемент метаданных, текст которого целиком разбирается как число с единицей («59 млн», «5,1 млн», «123 тыс.», «1.2M»), вне блоков VidIQ |
outlierScore |
Параметр outlier_score в ссылке внутри [data-hover-popover="OutlierPopover"] (точное значение, например 146.19). Если параметра нет, то число из текста значка (>100x → 100) |
subscribers |
Текст [data-vidiq-subscriber-count] («571 тыс.» → 571000), null, если элемента нет |
Архитектура
| Часть | Файл | Ответственность |
|---|---|---|
| Content script | src/content.js |
Наблюдает за DOM на youtube.com, разбирает карточки, отправляет в background только карточки, где VidIQ уже вывел score, и только когда данные изменились |
| Service worker | src/background.js |
Принимает карточки, подтягивает настройки и ранее сохранённые записи, пишет результат в IndexedDB. Записи обрабатываются строго по очереди |
| Разбор карточки | src/lib/card.js |
scrapeCard(card, link, context): достаёт поля из DOM-элемента карточки |
| Данные VidIQ | src/lib/vidiq.js |
Селекторы VidIQ, outlierScoreFromHref, parseVidiqScore, readVidiqData |
| Числа | src/lib/numbers.js |
parseCount, parseViews: «59 млн», «571 тыс.», «1.2M», «12 345» → число |
| Ссылки | src/lib/video-id.js |
isVideoId, parseVideoLink, pageFromPath |
| Запись | src/lib/record.js |
shouldSave, sanitizeItem (проверка входящего сообщения), buildRecord (создание и обновление записи) |
| Настройки | src/lib/storage.js |
Значения по умолчанию, normalizeSettings, чтение и запись настроек |
| База | src/lib/db.js |
Обёртка над IndexedDB, одно хранилище outliers |
| Экспорт | src/lib/export.js |
JSON и CSV |
| Popup | src/popup/ |
Счётчик, список последних находок, кнопки экспорта |
| Options | src/options/ |
Порог score и минимум просмотров |
Поток данных
- Content script запускает сканирование не чаще раза в 500 мс: после изменения DOM (
MutationObserver) и после событияyt-navigate-finish(YouTube — SPA). Это троттлинг, а не дебаунс: пока играет видео, DOM меняется постоянно. После навигации список уже отправленного сбрасывается. - Для каждой ссылки
/watch?v=и/shorts/ищется ближайший элемент карточки, иscrapeCardчитает поля. Карточка пропускается, если нетoutlierScore(VidIQ ещё не отрисовал блок: она подхватится при следующем сканировании) или нетviews. - Для каждой карточки считается подпись
outlierScore|views|subscribers. Карточка отправляется, только если подпись изменилась с прошлой отправки: VidIQ может дорисовать или обновить числа позже. - Background проверяет сообщение (
sanitizeItem), читает настройки и существующие записи, для каждой карточки вызываетbuildRecord, и записывает результат в IndexedDB. buildRecord: новое видео сохраняется, если выполнено условие порога. Уже сохранённое видео обновляется всегда, даже если его score упал ниже порога.
Модель данных
Хранилище outliers (ключ — videoId, индекс по updatedAt):
| Поле | Описание |
|---|---|
videoId |
ID видео |
title |
Название (не длиннее 300 символов) |
url |
https://www.youtube.com/watch?v=<videoId> для всех видео, включая Shorts |
thumbnailUrl |
Ссылка на превью |
channelTitle, channelUrl |
Канал (null, если не удалось определить) |
subscribers |
Число подписчиков от VidIQ или null |
views |
Просмотры на момент последней проверки |
outlierScore |
Outlier score от VidIQ, до двух знаков |
isShort |
true, если видео хотя бы раз встретилось по ссылке /shorts/ |
foundOn |
Страница первой находки: home, watch, search или channel |
firstSeenAt, updatedAt |
Временные метки (ISO). firstSeenAt не меняется при обновлении |
CSV содержит эти же поля в том же порядке.
Настройки (chrome.storage.local, ключ settings)
| Поле | По умолчанию |
|---|---|
threshold |
5 |
minViews |
10 000 |
Граничные случаи
| Ситуация | Поведение |
|---|---|
| VidIQ не установлен или ещё не отрисовал блок | Карточка пропускается и проверяется при следующем сканировании. Если VidIQ нет совсем, ничего не сохраняется, popup подсказывает про VidIQ |
| Не удалось разобрать просмотры (другой язык, прямой эфир) | Карточка пропускается |
| VidIQ скрыл подписчиков | subscribers: null, видео сохраняется по score |
| Повторная встреча видео | Обновляются views, outlierScore, subscribers, updatedAt. firstSeenAt и foundOn не меняются, isShort может только стать true |
| Расширение перезагружено | Старый content script отключается сам |
| Сообщение с некорректными данными | Отбрасывается в sanitizeItem: проверяются ID, числа, ссылки (youtube.com, i.ytimg.com) |
| YouTube или VidIQ поменяли разметку | Тест на фикстурах в реальном Chrome покажет, что именно перестало разбираться |
Тестирование
- Автотесты на встроенном
node:test, без зависимостей:numbers.js,vidiq.js(на подставных объектах),record.js,video-id.js,export.js,storage.js. - Тест разбора карточки в Chrome (
test/card.dom.test.js): запускает headless Chrome, загружает фикстуры и проверяетscrapeCard. Фикстуры: настоящая карточка с VidIQ с главной, плюс упрощённые карточки поиска и боковой панели. Если Chrome не найден, тест пропускается. - Сквозная проверка в Chrome (не входит в репозиторий): живой YouTube с подмешанным в карточки блоком VidIQ, проход через content script, background, IndexedDB и popup.
Вне объёма (YAGNI)
- Свой расчёт score, обращения к YouTube Data API и любым другим сетевым источникам
- Открытое видео на странице просмотра (собираются только карточки)
- Языки интерфейса кроме русского и английского
- Точные просмотры и подписчики (берутся значения с экрана)
- SQLite, синхронизация между устройствами, публикация в Chrome Web Store