Files
outlier-saver-extension/docs/superpowers/specs/2026-10-25-outlier-saver-design.md
T
2026-10-25 10:48:07 +03:00

13 KiB

Outlier Saver — дизайн расширения

Дата: 2026-10-25

Цель

Расширение для Chrome (Manifest V3), которое работает, пока пользователь просто смотрит YouTube: само находит видео с аномально высокими просмотрами относительно размера канала («outliers») и сохраняет их локально. Позже данные выгружаются в JSON или CSV.

Принятые решения

Вопрос Решение
Источник outlier score Гибрид: свой расчёт + значок VidIQ, если он есть на карточке
Хранение IndexedDB в браузере, экспорт в JSON и CSV из popup (SQLite не используем)
Формула score = просмотры ÷ подписчики
Страницы Главная (лента), страница просмотра, поиск, страница канала
Источник цифр YouTube Data API v3 (нужен ключ пользователя)
Сборка Нет. Чистый JS, ES-модули, загрузка как «распакованное расширение»

Архитектура

Часть Файл Ответственность
Content script src/content.js Следит за DOM на youtube.com, достаёт ID видео из ссылок, читает значок VidIQ, шлёт пачки в background
Service worker src/background.js Очередь ID, цикл сброса пачками, статус и бейдж на иконке, реакция на смену ключа
Обработка пачки src/lib/process.js processBatch: проверка seen, запросы к API, расчёт score, решение «сохранять или нет», запись в БД. Зависимости (БД и API) передаются параметрами, поэтому логика покрыта тестами
Настройки и статус src/lib/storage.js Значения по умолчанию, normalizeSettings, чтение и запись настроек и статуса в chrome.storage.local
Формула и решение src/lib/score.js Чистые функции: computeScore, shouldSave
Разбор ссылок src/lib/video-id.js Извлечение ID из /watch?v=, /shorts/, отсев мусора, определение типа страницы по пути
Значок VidIQ src/lib/vidiq.js Селектор значка (одна константа) и parseVidiqScore(text)
API src/lib/api.js videos.list, channels.list, разбиение на пачки по 50, разбор ошибок
База src/lib/db.js Обёртка над IndexedDB
Экспорт src/lib/export.js Сериализация в JSON и CSV
Popup src/popup/ Счётчик, список последних находок, кнопки экспорта, статус сбора
Options src/options/ API-ключ, порог score, минимум просмотров

Ключ API хранится в chrome.storage.local и читается только в background и на странице настроек. Content script его не использует. Запросы к API идут из background, а не со страницы, поэтому CORS не мешает.

Поток данных

  1. Content script на youtube.com реагирует на событие yt-navigate-finish (YouTube — SPA, страницы не перезагружаются) и на MutationObserver по документу. Сканирование запускается не чаще раза в 500 мс (троттлинг, а не дебаунс: пока играет видео, DOM меняется постоянно, и дебаунс не сработал бы). После навигации список уже отправленных ID сбрасывается.
  2. Из ссылок a[href*="/watch?v="] и a[href*="/shorts/"] через video-id.js берутся ID. Для каждой карточки пробуется прочитать значок VidIQ. Уже отправленные с этой страницы ID запоминаются в Set, чтобы не спамить сообщениями.
  3. Content script шлёт background сообщение {videoId, vidiqScore | null, viaShortsUrl, page}, где page — home | watch | search | channel.
  4. Background копит ID и сбрасывает очередь раз в секунду либо при накоплении 50 штук. ID, проверенные менее 24 часов назад (seen.checkedAt), пропускаются, кроме ID со значком VidIQ: значок может отрисоваться позже первого сканирования, поэтому content script отправляет ID повторно, когда у него появился score, а проверка seen для таких ID не применяется.
  5. videos.list (part=snippet,statistics,contentDetails) возвращает название, превью, просмотры, дату публикации, длительность и channelId.
  6. Для каналов, которых нет в кэше channels или чья запись старше 24 часов, вызывается channels.list (part=statistics,snippet).
  7. score = views / max(subscribers, 1). Если подписчики скрыты, score = null.
  8. Решение о сохранении (shouldSave): видео сохраняется, если views ≥ minViews и score ≥ threshold, или если vidiqScore не null. Условие по VidIQ действует независимо от своего порога и от minViews. Уже сохранённое видео обновляется при каждой проверке, даже если его score опустился ниже порога.
  9. Результат пишется upsert-ом в outliers, ID с временем проверки — в seen (в том числе несохранённые, чтобы не запрашивать их повторно).

Модель данных

Хранилище outliers (ключ — videoId)

Поле Описание
videoId ID видео
title Название
url https://www.youtube.com/watch?v=<videoId> для всех видео, включая Shorts
thumbnailUrl Лучшее доступное превью в порядке maxres → standard → high → medium → default
channelId, channelTitle, channelUrl Канал
subscribers Число подписчиков или null, если скрыто. API округляет до трёх значащих цифр
views Просмотры на момент последней проверки
publishedAt ISO-дата публикации
durationSec Длительность
isShort true, если хотя бы раз видео встретилось по ссылке /shorts/
score Свой score, округлённый до двух знаков, или null
vidiqScore Число со значка VidIQ или null
foundOn Страница первой находки
firstSeenAt, updatedAt Временные метки (ISO). firstSeenAt не меняется при upsert

Хранилище channels (ключ — channelId)

channelId, title, subscribers (число или null), fetchedAt.

Хранилище seen (ключ — videoId)

videoId, checkedAt, status (ok или unavailable). После 24 часов запись считается устаревшей и видео запрашивается заново. Это нужно, чтобы видео, которое стало outlier позже, не потерялось, а просмотры у сохранённых обновлялись.

Настройки (chrome.storage.local)

Ключ По умолчанию
apiKey пусто
threshold 5
minViews 10 000

Ошибки и граничные случаи

Ситуация Поведение
Ключ API не задан Сбор приостановлен, на иконке бейдж «!», popup ведёт в настройки. В seen ничего не пишется, после ввода ключа видео подхватятся
Квота исчерпана (403 quotaExceeded) Пауза до сброса квоты (полночь по Тихоокеанскому времени), popup показывает причину. Необработанные ID в seen не попадают
Невалидный ключ (400 или 403 keyInvalid) Как при отсутствии ключа, с сообщением об ошибке ключа
Сеть или 5xx Три повтора с нарастающей задержкой, затем ID пропускается без записи в seen
Скрытые подписчики score = null, видео сохраняется только при наличии vidiqScore
Очень мало подписчиков Делитель max(subs, 1), мусор отсекается через minViews
Приватное или удалённое видео API его не возвращает; в seen пишется status: unavailable, повторная проверка после TTL
Service worker выгрузили Очередь живёт в памяти и может потеряться. seen пишется только после результата, а content script присылает ID снова после следующей навигации по YouTube или перезагрузки страницы
Повторная встреча видео Upsert: обновляются views, subscribers, score, vidiqScore (если он есть сейчас), updatedAt; isShort может только стать true

Расход квоты

videos.list и channels.list стоят по 1 единице за запрос на 50 ID при лимите 10 000 единиц в сутки. Квота не должна быть проблемой при обычном просмотре.

Значок VidIQ: известное ограничение

Точная разметка значка VidIQ на карточке неизвестна, а доступа к живой странице с установленным VidIQ у разработки нет. Поэтому:

  • селектор и разбор текста изолированы в src/lib/vidiq.js;
  • пока селектор не заполнен, vidiqScore всегда null, остальная логика работает без изменений;
  • последним шагом реализации пользователь показывает DOM карточки со значком VidIQ (правый клик → «Просмотреть код»), после чего селектор и parseVidiqScore заполняются и покрываются тестом на реальном образце текста.

Тестирование

  • Автотесты на встроенном node:test, без зависимостей: score.js (формула, shouldSave на границах порогов и со скрытыми подписчиками), video-id.js (watch, shorts, мусорные ссылки), api.js (разбиение на пачки по 50, разбор ответов и ошибок на подставных данных), export.js (экранирование запятых, кавычек и переносов строк в названиях).
  • Ручной чек-лист на живом YouTube: подгрузка карточек при скролле на главной; переход между страницами без перезагрузки; работа на /results и /@channel/videos; запись появляется в popup; скачивание JSON и CSV; поведение без ключа и с неверным ключом.

Вне объёма (YAGNI)

  • SQLite и запись на диск через native messaging (можно добавить отдельным этапом, если экспорта станет мало)
  • Формула «просмотры ÷ медиана канала»
  • Синхронизация между устройствами
  • Публикация в Chrome Web Store