myYouTube/analytics/2026-09-17-project-baseline.md
vrubelroman 5fa5a391d7 Remove AGENT_TEAM.md, add project baseline analytics
The agent team workflow lives in .opencode/agent role files; the
human-readable guide is redundant and removed. README and analyst role
updated accordingly. Added analytics baseline documenting the current
project state as a starting point for future tasks.
2026-09-17 21:42:30 +00:00

25 KiB
Raw Blame History

Базовый документ проекта (baseline)

Задача

Зафиксировать фактическое состояние проекта MyYouTube на 2026-09-17 как точку отсчёта для будущих задач агентской команды. Документ описывает: обзор, архитектуру, интеграции, ключевые решения с причинами из git-истории, известные ограничения/техдолг и устройство команды агентов. Факты сверены с кодом, README.md, AGENTS.md, миграциями и git log.

1. Обзор проекта

Персональный single-user YouTube-клиент с категориями подписок и интеграцией с существующим MeTube. Скачивание выполняет MeTube на mediaVM; этот сервис ничего не скачивает сам и не хранит видео локально (только проксирует/линкует media_url).

  • Статус: функционально готово. Этапы 1–6 реализованы (каркас, OAuth, категории, синк подписок/видео, лента, воспроизведение, MeTube-интеграция), плюс доработки по итогам live-тестирования. Развёрнуто на тестовом окружении: testmyyoutube.vrubel.xyz → hermesVM 192.168.8.173 (порт 8080) → MeTube на mediaVM 192.168.8.177:8081.
  • Стек: Python 3.13 + FastAPI + SQLAlchemy 2.x + Alembic + Pydantic Settings + APScheduler; React 19 + TypeScript + Vite 8 + React Router 7 + TanStack Query 5; PostgreSQL 16; Docker Compose.
  • Масштаб: single-user — вход разрешён только ALLOWED_GOOGLE_EMAIL; фронтенд не ходит в Google API и MeTube write API напрямую (только через backend).
  • Реализованные возможности: Google OAuth (allow-list) → синк подписок (APScheduler) и видео (по активности); категории (CRUD, reorder, many-to-many с каналами, постоянные URL); лента с курсорной пагинацией, поиском по видео/каналам и фильтром «только новые»; YouTube-плеер; MeTube-интеграция (download/delete, Socket.IO live-статусы, recovery после рестарта); раздел «На сервере» (/local, перенаправление с /saved); отписка на YouTube; удаление скачанной копии; тёмный адаптивный UI с мобильным меню; скрытие отписанных каналов; счётчики подписчиков и бейджи «N новых» (окно 2 дня).
  • Быстрая проверка (prod): docker compose up -d --build → curl http://localhost:8080/api/health (возвращает status/database/metube).
  • Состояние worktree на момент фиксации: ветка master впереди origin/master на 1 коммит (f0ca7b9 не запушен); в работе документационная задача «Удалить AGENT_TEAM.md» (analytics/2026-09-17-remove-agent-team-guide.md, staged-удаление AGENT_TEAM.md, правки README.md и .opencode/agent/analyst.md).

2. Архитектура

Backend (backend/app/)

  • main.py — FastAPI app: SessionMiddleware (cookie myyoutube_session, 30 дней, same_site=lax, https_only по APP_BASE_URL), lifespan = reconcile download jobs по /history MeTube + старт APScheduler + Socket.IO-листенер MeTube (переподключения). Статика собранного фронта монтируется в / с fallback на index.html (SPAStaticFiles).
  • api/ (группы, кроме auth/status, auth/google/* и health — за require_session):
    • health — GET /api/health (status, database, metube).
    • auth — GET /auth/status, /auth/google/start, /auth/google/callback (state-проверка, allow-list, background initial sync), POST /auth/logout.
    • channels — GET /channels (фильтры subscribed/search/category_id/uncategorized), GET /channels/{id}, PUT /channels/{id}/categories, POST /channels/{id}/unsubscribe (реальная отписка на YouTube).
    • categories — CRUD + POST /categories/reorder (slug-генерация, проверка дублей имён).
    • feed — GET /feed (курсорная пагинация base64 published_at|id, фильтры category/uncategorized/channel/downloaded/search/new_only, лимит 30/100), GET /feed/saved-counts.
    • videos — GET /videos/{youtube_video_id}, POST .../download, DELETE .../download, GET .../download-status, POST .../recheck-local.
    • sync — POST /sync/subscriptions, POST /sync/videos, GET /sync/status.
  • services/ — google_oauth (flow, allow-list, кэш токена), youtube_client (все вызовы YouTube API, ошибки квоты/scope), sync (подписки и видео, thread-lock'и), sync_trigger (синк по активности), scheduler (APScheduler, только подписки), metube_client (HTTP+Socket.IO к MeTube), download_jobs (машина состояний, reconcile), state (AppSetting get/set), video_presentation (сериализация).
  • core/ — auth_dependency.require_session (+триггер синка видео), crypto (Fernet для refresh token), duration (ISO 8601 → секунды), slugify.
  • Модели БД: AppSetting (key/value — статусы синков), OAuthCredentials (singleton id=1, encrypted_refresh_token), Channel (youtube_channel_id, youtube_subscription_id, uploads_playlist_id, subscriber_count, subscribed, last_synced_at), Video (youtube_video_id unique, published_at, duration_seconds, youtube_url), Category (name/slug unique, sort_order), channel_categories (m2m, PK-пара), DownloadJob (статусы queued/downloading/postprocessing/completed/failed/deleted/unknown, metube_job_id, metube_filename, media_url).
  • Миграции (Alembic, migrations/versions/): 0001 app_settings → 0002 oauth_credentials + channels → 0003 categories + channel_categories → 0004 videos (+индексы) → 0005 download_jobs → 0006 channels.youtube_subscription_id → 0007 drop access_token_expires_at (мёртвая после in-process кэша) → 0008 channels.subscriber_count (BigInteger, из statistics).

Frontend (frontend/src/)

  • Роуты (App.tsx): / Feed, /category/:categoryId, /uncategorized, /local (скачанные), /search, /saved → redirect /local, /channels, /channels/:channelId/videos, /video/:youtubeVideoId, /settings, /settings/categories (и /categories → redirect), 404. До входа — страница Connect.
  • Страницы: Connect, Feed (infinite-query, фильтры, бейджи новых, кнопка «Обновить»), Channels (поиск, CategoryNav, назначение категорий, отписка), ChannelVideos (счётчик подписчиков), VideoPage, Categories (CRUD, reorder, confirm-диалог), Settings (health MeTube, статусы синков, переподключение, logout).
  • Компоненты: AppShell (сайдбар с категориями и бейджами новых, поиск по видео/каналам, индикатор синка, мобильное drawer-меню), VideoCard, ChannelCard (popover категорий, создание категории на лету), DownloadButton (polling 2 с только для активных статусов), Player (локальная копия через media_url, при ошибке — fallback на iframe youtube-nocookie.com + recheck-local), Icon.
  • API-клиент — api/client.ts, fetch с credentials: include, все эндпоинты типизированы; utils/format.ts — форматирование длительности/времени/счётчиков подписчиков.
  • Стили/состояние: App.css/index.css (тёмная адаптивная тема); TanStack Query с invalidation по ключам ['feed'], ['channels'], ['categories'], ['download-status', id] и т.д.; позиция скролла ленты восстанавливается через sessionStorage (feed-scroll:*).

Деплой

  • Dockerfile — multi-stage: node:22-slim собирает фронт (npm ci && npm run build), python:3.13-slim — бэкенд; dist → backend/static; non-root пользователь uid 10001; EXPOSE 8080.
  • entrypoint.sh — alembic upgrade head → uvicorn app.main:app (--proxy-headers --forwarded-allow-ips="*", порт APP_PORT).
  • compose.yml — app (env_file .env, healthcheck curl /api/health) + postgres:16 (volume postgres_data, healthcheck pg_isready). Миграции применяются при старте контейнера.
  • .env — секреты: APP_SECRET_KEY, TOKEN_ENCRYPTION_KEY (Fernet, менять нельзя), GOOGLE_CLIENT_SECRET, POSTGRES_PASSWORD; все внешние URL (Google/YouTube/MeTube) и параметры синка — в env (см. .env.example).
  • Локальная разработка: backend — uv venv/uv pip install -r requirements-dev.txt + alembic upgrade head + uvicorn app.main:app --app-dir backend --port 8080; тесты — pytest tests/ -v (SQLite in-memory, Google/MeTube mock). Frontend — npm install + npm run dev, Vite проксирует /api/* на 127.0.0.1:8080 (vite.config.ts); lint — oxlint, сборка — tsc -b && vite build.

Ключевые настройки (config.py, все переопределяются env)

  • Google/YouTube endpoints: google_auth_uri, google_token_uri, google_userinfo_uri, google_revoke_uri, youtube_api_base_url (https://www.googleapis.com/youtube/v3), youtube_watch_url_template.
  • MeTube: metube_api_base_url (дефолт http://127.0.0.1:8081, реально http://192.168.8.177:8081), metube_public_base_url (для браузера), metube_container_download_dir (/downloads), metube_request_timeout_seconds (30).
  • Синк: subscriptions_sync_interval_hours=6 (APScheduler), videos_sync_idle_hours=2 (триггер по активности), videos_backfill_cap=200, videos_known_stop_threshold=50, new_videos_window_days=2.

3. Интеграции

Google OAuth / синк

  • Scope: полный https://www.googleapis.com/auth/youtube (read/write — ради subscriptions.delete при отписке; сознательное отступление от youtube.readonly по явному запросу пользователя) + openid, userinfo.email, userinfo.profile. Старые readonly-токены не покрывают отписку → нужен один переподключение.
  • Single-user: email из userinfo сверяется с ALLOWED_GOOGLE_EMAIL; недопустимый аккаунт — revoke_token + редирект с auth_error=account_not_allowed. Redirect URI фиксирован: {APP_BASE_URL}/api/auth/google/callback (должен быть в Authorized redirect URIs Google-клиента).
  • Кэш токена: refresh token хранится в БД шифрованным (Fernet, TOKEN_ENCRYPTION_KEY); access token — в памяти процесса под threading.Lock, проактивный refresh за 60 с до expiry (OAUTHLIB_RELAX_TOKEN_SCOPE=1 снимает ложный scope-mismatch при расширении scope). Обмен кода на токен с access_type=offline, prompt=consent.
  • После подключения — фоновый initial sync (подписки + видео) через BackgroundTasks.

YouTube API (youtube_client.py)

  • Ресурсы: subscriptions.list (mine=true, пагинация по 50), channels.list (snippet,contentDetails,statistics — uploads playlist и подписчики, батчи по 50), playlistItems.list (contentDetails, лимит MAX_PLAYLIST_PAGES=10 страниц), videos.list (snippet,contentDetails,status, батчи по 50), subscriptions.delete.
  • Ошибки → HTTP: quotaExceeded/dailyLimitExceeded/rateLimitExceeded → 503 «quota exhausted»; insufficient scope → 403 с подсказкой переподключиться; прочее → 502.
  • Квота (оценка из паттерна вызовов): видео-синк на канал — 1 юнит playlistItems.list + 1 юнит videos.list на каждые ≤50 новых id → практически ~2 юнита на канал за синк видео при малом числе новых видео; синк подписок — 1 юнит subscriptions.list на 50 подписок + 1 юнит channels.list на 50 каналов. Расход пропорционален числу каналов.

MeTube (metube_client.py)

  • Весь HTTP/Socket.IO к MeTube инкапсулирован в MeTubeClient (правило AGENTS.md): POST /add (url, download_type=video, codec auto, format mp4, quality best, auto_start, custom_name_prefix = youtube_video_id), GET /history, POST /delete (ключ — URL, не job id; where=done), build_media_url (публичный /download/<filename> с защитой от path traversal), check_media (HEAD), health.
  • Socket.IO-события added/updated/completed/canceled/cleared (payload — JSON-строка от DownloadInfo.to_public_dict()); маппинг статусов MeTube (pending/preparing/scheduled/downloading/postprocessing/finished/error) → свои. Только событие completed — авторитетный терминальный статус (yt-dlp шлёт finished по отдельным потокам → иначе flapping). media_url строится из точного filename события (не из title) и отдаётся только при completed.
  • Recovery: при старте reconcile_on_startup сверяет активные/unknown job'ы с /history; completed с media_url не трогаются. «cleared»/«canceled» матчатся по URL, несовпавшие игнорируются.

Процессы синка (services/sync.py)

  • Подписки: subscriptions.list (пагинация) → upsert каналов (заполняется и youtube_subscription_id для будущей отписки); каналы, которых нет в ответе API, помечаются subscribed=False (скрываются в UI). Затем батчами по 50 channels.list добираются uploads_playlist_id и subscriber_count; неудача этой фазы не валит весь синк. Запуск: APScheduler каждые 6 ч + ручной POST /sync/subscriptions; параллельный запуск блокируется threading.Lock → 409 SyncInProgress.
  • Видео: по каждому подписанному каналу с uploads-плейлистом — инкрементальный playlistItems.list (ранняя остановка на 50 подряд известных, кап 200 новых на канал, максимум 10 страниц на плейлист); известные id собраны заранее одним запросом к БД (без N+1). Для неизвестных id — videos.list батчами по 50 и insert. Существующие видео не обновляются. Запуск: триггер по активности (maybe_trigger_videos_sync в require_session, idle ≥2 ч, фоновый поток) + ручной POST /sync/videos.
  • Статусы синков персистятся в app_settings (JSON: status/started_at/finished_at/error + счётчики), отдаются через GET /api/sync/status (плюс флаг running из lock'а); UI опрашивает каждые 2 с пока идёт синк, иначе раз в 60 с.

4. Ключевые решения и политики (из git log)

  • 2026-09-16, 0ed20bb — этапы 1–5: каркас, OAuth, категории, синк видео/лента, воспроизведение.
  • 2026-09-16, fe16c08 — этап 6: MeTube-интеграция (download/delete, live-статусы).
  • 2026-09-16, e333296/10c16ba — статусы загрузки: терминален только completed; кнопка не откатывается в «Скачать» после завершения.
  • 2026-09-16, 3089202+8ddea87 — реальная отписка (subscriptions.delete) и удаление скачанного — сверх MVP по запросу пользователя; из-за этого scope расширен до полного youtube (потребовался OAUTHLIB_RELAX_TOKEN_SCOPE).
  • 2026-09-16, 62f8fb7/441a3ef — DELETE /videos/{id}/download: ключ /delete — URL (по id — тихий no-op); после запроса проверяем фактическое удаление файла (настройка DELETE_FILE_ON_TRASHCAN MeTube вне нашего контроля → 409).
  • 2026-09-16, b29bcfb…90eb012 — отписанные каналы скрываются; вкладка Saved; счётчики каналов/сохранённых в сайдбарах.
  • 2026-09-16, c4a772b — тёмный адаптивный редизайн интерфейса.
  • 2026-09-16, 6c704ca — переход на opencode-команду агентов (Codex/Herdr выброшены).
  • 2026-09-17, fde9a43 — review-фиксы: in-process кэш access token (+миграция 0007), обработка cleared/canceled по URL, email в /auth/status только при валидной сессии, Google base URLs в настройки, non-root контейнер; убран «Без категории» из сайдбара (фильтр остался на странице каналов), локальные фильтры в query key, Content-Type только с телом.
  • 2026-09-17, e10df8d — канальные фичи: подписчики из statistics (0008), бейджи «N новых» (окно NEW_VIDEOS_WINDOW_DAYS=2), синк видео по активности (idle VIDEOS_SYNC_IDLE_HOURS=2 вместо ежечасного планировщика), инкрементальная догрузка: ранняя остановка после 50 подряд известных id (VIDEOS_KNOWN_STOP_THRESHOLD), жёсткий кап 200 последних видео на канал (VIDEOS_BACKFILL_CAP); клик по бейджу фильтрует категорию (new_only).
  • 2026-09-17, 2e5ef8e — .env.example с комментариями к каждому полю.
  • 2026-09-17, f0ca7b9 — введён субагент Analyst и конвенция analytics/.

5. Известные ограничения и техдолг

  • Квота YouTube: основной расход — по каналу за видео-синк (~1–2 юнита, см. п. 3); при текущем числе подписок практический запас — порядка 2–4 полных синков видео в день (оценка, зависит от числа каналов). Исчерпание → 503, обновления пропускаются до сброса суточной квоты; ручной «Обновить» в UI не «пробьёт» квоту.
  • Браузерная проверка только пользователем: в команде нет Playwright/скриншот-инструментов; reviewer/tester честно помечают визуальное как неверифицированное, финальную визуальную приёмку делает пользователь после деплоя.
  • Edge-кейсы MeTube: удаление записи вне нашего приложения (cleared done-записи) матчится по URL и может быть не связано с нашими job'ами; re-download после удаления создаёт новый DownloadJob, значимым считается последний; после рестарта активные job'ы сверяются с /history (иначе → unknown, пользователь видит «Загрузить снова»).
  • Отсутствует CI и E2E: тесты — только backend pytest (tests/, 12 файлов, SQLite in-memory, Google/MeTube замоканы; реальные вызовы к MeTube — opt-in). GitHub Actions нет, E2E/снапшотов нет.
  • Single-worker lock: защита от параллельных синков — threading.Lock в одном процессе (uvicorn запускается одним worker'ом); при scale-out и триггер по активности, и блокировки перестанут быть глобальными — сознательное упрощение single-user сервиса.
  • Синк видео не обновляет метаданные уже известных видео (videos_updated всегда 0, обновляются только новые id).
  • Сознательно не реализовано: YouTube Recommendations/Home (запрещено), PWA, watch later, Shorts-фильтр, SponsorBlock; полная история канала не бэкапится (кап 200).
  • Удаление файла на диске зависит от DELETE_FILE_ON_TRASHCAN MeTube (вне нашего контроля, правило 11); чужие (не наши) файлы MeTube не удаляем никогда (правило 10).

6. Команда агентов

  • Конфиг: opencode.json → default_agent: orchestrator. Работа — в одной сессии opencode; задача пишется обычным сообщением.
  • Роли (.opencode/agent/): orchestrator (primary; декомпозиция, единственный говорит с пользователем, собирает отчёт, деплой на тестовый сервис docker compose up -d --build после закрытия Critical/Major), analyst (subagent; только документация analytics/** и точечные правки README.md), coder (subagent; единственный меняет код продукта), reviewer (subagent, edit: deny; read-only review диффа, severity Critical/Major/Minor), tester (subagent, edit: deny; прогон проверок, ничего не чинит).
  • Цикл: orchestrator → analyst (фиксация задачи) → coder → reviewer + tester (параллельно) → follow-up по находкам → деплой → отчёт пользователю. Отчёты субагентов заканчиваются маркерами ANALYST_DONE / CODER_DONE / REVIEW_DONE / TEST_DONE. Коммит/пуш — только по явному запросу.
  • Конвенция analytics/ (analytics/README.md): один файл на задачу analytics/<YYYY-MM-DD>-<slug>.md; секции: Задача, Контекст, Затронутые подсистемы и файлы, Критерии приёмки, План, Риски и ограничения, Журнал изменений; follow-up дополняет существующий файл (журнал + только затронутые секции); документы на русском, кратко, но полно; код не пишется.

Журнал изменений

  • 2026-09-17 — создан baseline-документ (Analyst). Сверено с кодом: модули backend/frontend, все миграции 0001–0008, настройки .env.example/config.py, git log -25, роли агентов.