Files
outlier-saver-extension/docs/superpowers/specs/2026-10-25-outlier-saver-design.md
T

114 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 и минимум просмотров |
## Поток данных
1. Content script запускает сканирование не чаще раза в 500 мс: после изменения DOM (`MutationObserver`) и после события `yt-navigate-finish` (YouTube — SPA). Это троттлинг, а не дебаунс: пока играет видео, DOM меняется постоянно. После навигации список уже отправленного сбрасывается.
2. Для каждой ссылки `/watch?v=` и `/shorts/` ищется ближайший элемент карточки, и `scrapeCard` читает поля. Карточка пропускается, если нет `outlierScore` (VidIQ ещё не отрисовал блок: она подхватится при следующем сканировании) или нет `views`.
3. Для каждой карточки считается подпись `outlierScore|views|subscribers`. Карточка отправляется, только если подпись изменилась с прошлой отправки: VidIQ может дорисовать или обновить числа позже.
4. Background проверяет сообщение (`sanitizeItem`), читает настройки и существующие записи, для каждой карточки вызывает `buildRecord`, и записывает результат в IndexedDB.
5. `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