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.
25 KiB
25 KiB
Базовый документ проекта (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→ hermesVM192.168.8.173(порт 8080) → MeTube на mediaVM192.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(cookiemyyoutube_session, 30 дней,same_site=lax,https_onlyпоAPP_BASE_URL), lifespan = reconcile download jobs по/historyMeTube + старт 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(курсорная пагинация base64published_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(singletonid=1,encrypted_refresh_token),Channel(youtube_channel_id,youtube_subscription_id,uploads_playlist_id,subscriber_count,subscribed,last_synced_at),Video(youtube_video_idunique,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/): 0001app_settings→ 0002oauth_credentials+channels→ 0003categories+channel_categories→ 0004videos(+индексы) → 0005download_jobs→ 0006channels.youtube_subscription_id→ 0007 dropaccess_token_expires_at(мёртвая после in-process кэша) → 0008channels.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 на iframeyoutube-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, healthcheckcurl /api/health) +postgres:16(volumepostgres_data, healthcheckpg_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сверяет активные/unknownjob'ы с/history; completed сmedia_urlне трогаются. «cleared»/«canceled» матчатся по URL, несовпавшие игнорируются.
Процессы синка (services/sync.py)
- Подписки:
subscriptions.list(пагинация) → upsert каналов (заполняется иyoutube_subscription_idдля будущей отписки); каналы, которых нет в ответе API, помечаютсяsubscribed=False(скрываются в UI). Затем батчами по 50channels.listдобираютсяuploads_playlist_idиsubscriber_count; неудача этой фазы не валит весь синк. Запуск: APScheduler каждые 6 ч + ручнойPOST /sync/subscriptions; параллельный запуск блокируетсяthreading.Lock→ 409SyncInProgress. - Видео: по каждому подписанному каналу с 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_TRASHCANMeTube вне нашего контроля → 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), синк видео по активности (idleVIDEOS_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: удаление записи вне нашего приложения (
cleareddone-записи) матчится по 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_TRASHCANMeTube (вне нашего контроля, правило 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, роли агентов.