commit 0ed20bb83812e33ecbbdcac39855d1f665dc1a23 Author: vrubelroman Date: Wed Sep 16 18:44:30 2026 +0000 Implement Phases 1-5: skeleton, OAuth, categories, video sync/feed, playback - FastAPI + PostgreSQL + Alembic + React/Vite skeleton, Docker Compose, healthcheck - Google OAuth (single allowed account), encrypted refresh token storage - Subscriptions sync with pagination, uploads playlist batch fetch - Categories CRUD, many-to-many channel assignment, category filtering - Video sync (playlistItems + videos.list batching), cached feed with cursor pagination, background scheduler (APScheduler) - Video detail page with YouTube embed player - SPA fallback routing, optimistic UI updates, client-side query caching 40 backend tests covering OAuth allow-list, sync idempotency, cascade deletes, cursor pagination, and category filtering. Co-Authored-By: Claude Sonnet 5 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..499d2fb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.env +**/__pycache__ +**/*.pyc +frontend/node_modules +frontend/dist +backend/.venv +tests +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6c36c8a --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +APP_ENV=production +APP_BASE_URL=https://testmyyoutube.vrubel.xyz +APP_PORT=8080 +APP_SECRET_KEY=CHANGE_ME +TOKEN_ENCRYPTION_KEY=CHANGE_ME + +POSTGRES_PASSWORD=CHANGE_ME +DATABASE_URL=postgresql+psycopg://youtube_app:CHANGE_ME@postgres:5432/youtube_app + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI=https://testmyyoutube.vrubel.xyz/api/auth/google/callback +ALLOWED_GOOGLE_EMAIL= + +METUBE_API_BASE_URL=http://192.168.8.177:8081 +METUBE_PUBLIC_BASE_URL=http://192.168.8.177:8081 +METUBE_CONTAINER_DOWNLOAD_DIR=/downloads +METUBE_REQUEST_TIMEOUT_SECONDS=30 + +SUBSCRIPTIONS_SYNC_INTERVAL_HOURS=6 +VIDEOS_SYNC_INTERVAL_MINUTES=60 +VIDEOS_PER_CHANNEL_SYNC=10 + +LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af58f7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.env +.env.local + +__pycache__/ +*.pyc +backend/.venv/ +.venv/ + +frontend/node_modules/ +frontend/dist/ + +*.log +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b1c87dd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# AGENTS.md + +Полное ТЗ: [`youtube_categories_metube_TZ.md`](./youtube_categories_metube_TZ.md). Перед любой существенной работой сверяйся с ним — этот файл лишь выжимка ограничений. + +## Жёсткие ограничения (раздел 34 ТЗ) + +1. Не форкать/копировать Tube Archivist. +2. Не встраивать yt-dlp в этот сервис — скачивание только через существующий MeTube (`mediaVM`). +3. Не дублировать функции скачивания MeTube. +4. Не монтировать media storage MeTube на `hermesVM` — видео не хранится локально, только проксируется/линкуется. +5. Не реализовывать YouTube Recommendations/Home. +6. Не делать multi-user — приложение строго single-user (`ALLOWED_GOOGLE_EMAIL`). +7. Frontend не должен напрямую вызывать Google API. +8. Frontend не должен напрямую вызывать MeTube write API (`POST /add` и т.п.) — только через наш backend. +9. Не восстанавливать filename скачанного видео из YouTube title — использовать только точное имя, пришедшее от MeTube по событию `completed`. +10. Не удалять существующие файлы MeTube. +11. Не менять исходный код MeTube. +12. Все внешние base URL (MeTube, Google) — через env/config, не хардкодить. +13. REST API должен оставаться пригодным для будущего mobile/PWA клиента. + +## Порядок фаз (раздел 33 ТЗ) + +Не реализовывать следующую фазу, пока не работает предыдущая: + +1. Skeleton (текущая фаза) — FastAPI, PostgreSQL, Alembic, React, Docker Compose, healthcheck. +2. Google OAuth + subscriptions sync + channels UI. +3. Categories CRUD + many-to-many + фильтр по категориям. +4. Video sync/feed (uploads playlists, playlistItems, videos, background scheduler). +5. YouTube playback (embed). +6. MeTube integration (`/add`, download_jobs, Socket.IO consumer, media URL). +7. Hardening (retry/recovery, logging, quota handling, tests, README). + +## Структура и соглашения + +- `migrations/` — Alembic, живёт в корне репозитория (не внутри `backend/`), импортирует модели из `backend/app`. +- Схема БД добавляется миграциями инкрементально по фазам (см. раздел 11 ТЗ), а не одним махом в Phase 1. +- MeTube-специфичные HTTP/Socket.IO вызовы должны быть инкапсулированы в отдельный класс `MeTubeClient` (раздел 36 ТЗ) — не размазывать по backend. +- Секреты только через `.env` (см. `.env.example`), никогда не коммитить `.env`, refresh token, client secret. +- Backend тесты — `pytest`, лежат в `tests/` в корне. Integration-тесты против Google/MeTube — mocked по умолчанию; реальные вызовы к `http://192.168.8.177:8081` — только opt-in, не в обычном CI. + +## Запуск/проверка + +См. `README.md`. Быстрая проверка: `docker compose up -d --build` и `curl http://localhost:8080/api/health`. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94050c0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:22-slim AS frontend-build +WORKDIR /frontend +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +FROM python:3.13-slim AS backend +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +COPY backend/requirements.txt ./backend/requirements.txt +RUN pip install --no-cache-dir -r backend/requirements.txt + +COPY backend/ ./backend/ +COPY migrations/ ./migrations/ +COPY alembic.ini ./alembic.ini +COPY --from=frontend-build /frontend/dist ./backend/static + +ENV PYTHONPATH=/app/backend +WORKDIR /app + +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh + +EXPOSE 8080 + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c7d962 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# MyYouTube + +Персональный YouTube-клиент с категориями подписок и интеграцией с существующим MeTube. +Полное техническое задание: [`youtube_categories_metube_TZ.md`](./youtube_categories_metube_TZ.md). + +Текущий статус: **Phase 1 — skeleton** (FastAPI backend, PostgreSQL, Alembic, React frontend, Docker Compose, healthcheck). + +## Стек + +- Backend: Python 3.13, FastAPI, SQLAlchemy 2.x, Alembic, Pydantic Settings +- Frontend: React, TypeScript, Vite, React Router, TanStack Query +- DB: PostgreSQL 16 +- Deployment: Docker Compose + +## Запуск в production (Docker Compose) + +1. Скопируй `.env.example` в `.env` и заполни значения (Google OAuth credentials, секреты, MeTube URLs). +2. Собери и запусти: + + ```bash + docker compose up -d --build + ``` + +3. Проверь health: + + ```bash + curl http://localhost:8080/api/health + ``` + +Миграции Alembic применяются автоматически при старте контейнера `app` (см. `entrypoint.sh`). + +## Локальная разработка + +### Backend + +```bash +cd backend +uv venv .venv +uv pip install -p .venv -r requirements-dev.txt +source .venv/bin/activate +cd .. +DATABASE_URL=postgresql+psycopg://youtube_app:password@localhost:5432/youtube_app \ +APP_SECRET_KEY=dev APP_SECRET_KEY=dev TOKEN_ENCRYPTION_KEY=dev \ + alembic upgrade head +uvicorn app.main:app --reload --app-dir backend --port 8080 +``` + +Тесты: + +```bash +backend/.venv/bin/python -m pytest tests/ -v +``` + +### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +Dev-сервер Vite проксирует `/api/*` на `http://127.0.0.1:8080` (см. `vite.config.ts`). + +## Миграции + +```bash +alembic revision -m "описание" +alembic upgrade head +``` + +`alembic.ini` и папка `migrations/` находятся в корне проекта и подключают модели из `backend/app`. + +## Структура проекта + +```text +myyoutube/ +├── backend/ # FastAPI приложение +├── frontend/ # React SPA +├── migrations/ # Alembic migrations +├── tests/ # backend tests (pytest) +├── Dockerfile +├── compose.yml +├── .env / .env.example +└── youtube_categories_metube_TZ.md +``` + +## Секреты + +`.env` в `.gitignore`, не коммитится. Обязательные секреты: `APP_SECRET_KEY`, `TOKEN_ENCRYPTION_KEY`, `GOOGLE_CLIENT_SECRET`, `POSTGRES_PASSWORD`. diff --git a/UI_UX_spec.md b/UI_UX_spec.md new file mode 100644 index 0000000..3f2ad5c --- /dev/null +++ b/UI_UX_spec.md @@ -0,0 +1,1018 @@ +# UI/UX-спецификация: персональный YouTube-клиент + +**Назначение:** передать coding agent вместе с основным техническим заданием. +**Формат:** single-user self-hosted web app, в будущем PWA/mobile client. + +## 1. Концепция + +Приложение должно ощущаться как персональная лента YouTube-подписок, но с нормальной категоризацией каналов и локальным хранением выбранных видео. + +Пользовательский сценарий: + +```text +Открыть приложение +→ выбрать категорию +→ увидеть последние видео каналов этой категории +→ смотреть через YouTube +→ при желании нажать "Скачать" +→ после загрузки видеть "На сервере" +→ дальше смотреть локальную копию через MeTube +``` + +Интерфейс не должен выглядеть как admin panel. Это consumer media UI. + +--- + +## 2. Общий layout + +Desktop: + +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ Header │ +├──────────────────────┬───────────────────────────────────────────────┤ +│ Sidebar │ Main content │ +│ │ │ +│ Все видео │ Video feed │ +│ Без категории │ │ +│ На сервере │ │ +│ │ │ +│ КАТЕГОРИИ │ │ +│ Linux │ │ +│ Шахматы │ │ +│ Научпоп │ │ +│ │ │ +│ Каналы │ │ +│ Настройки │ │ +└──────────────────────┴───────────────────────────────────────────────┘ +``` + +Sidebar: 220–260 px. +Header: фиксированный сверху. +Main content: responsive grid. + +Mobile: sidebar превращается в drawer/hamburger. + +--- + +## 3. Визуальный стиль + +Тёмная тема по умолчанию. + +Рекомендуемые design tokens: + +```text +Background main #0F0F0F +Surface #181818 +Surface hover #272727 +Border #303030 +Primary text #F1F1F1 +Secondary text #AAAAAA +Accent #3EA6FF +Success/local #2BA640 +Warning/downloading #F5A623 +Error #E53935 +``` + +Использовать CSS variables, а не раскидывать цвета по компонентам. + +Типографика: + +```text +Inter / Roboto / system-ui +Page title 24px / 600 +Section title 18px / 600 +Video title 15–16px / 500 +Metadata 12–14px +Buttons 14px / 500 +``` + +--- + +## 4. Header + +Пример: + +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ ▶ MyTube 🔍 Поиск... ✓ 12 мин ⚙ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +Слева: +- логотип; +- рабочее название `MyTube` или configurable app name. + +Центр: +- поиск по локальной БД: video title + channel title. + +Справа: +- sync indicator; +- settings. + +Sync states: + +```text +✓ Обновлено 12 мин назад +↻ Обновление... +⚠ Ошибка синхронизации +``` + +По клику на sync indicator: + +```text +Подписки: обновлены 20:11 +Видео: обновлены 20:12 +MeTube: доступен +``` + +--- + +## 5. Sidebar + +Пример: + +```text +● Все видео + Без категории + На сервере + +КАТЕГОРИИ + +🐧 Linux +♟ Шахматы +🔬 Научпоп +🎮 Игры + ++ Новая категория + +──────────── + +Каналы +Настройки +``` + +Активная категория должна иметь заметный, но спокойный selected state. + +`Все видео`, `Без категории`, `На сервере` — виртуальные категории. + +--- + +## 6. Feed + +Route: + +```text +/ +``` + +или: + +```text +/feed +``` + +Header выбранной категории: + +```text +Linux +24 канала +Обновлено 12 минут назад [↻ Обновить] +``` + +Опциональные chips: + +```text +[Все] [На сервере] [Не скачаны] +``` + +Сортировка видео: + +```text +published_at DESC +``` + +--- + +## 7. Video grid + +Responsive: + +```text +>= 1800 px → 5 columns +>= 1400 px → 4 columns +>= 1000 px → 3 columns +>= 700 px → 2 columns +mobile → 1 column +``` + +Рекомендуемый CSS: + +```css +grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); +``` + +--- + +## 8. Video card + +Пример: + +```text +┌────────────────────────────────────┐ +│ │ +│ THUMBNAIL │ +│ 18:42 │ +│ │ +├────────────────────────────────────┤ +│ Linux Kernel 7.0 — What's New │ +│ The Linux Experiment │ +│ 2 часа назад │ +│ │ +│ [▶ Смотреть] [⬇ Скачать] │ +└────────────────────────────────────┘ +``` + +Thumbnail: +- aspect ratio 16:9; +- duration badge справа снизу. + +Карточка содержит: +- thumbnail; +- title; +- channel name; +- publish time; +- duration; +- download/local state; +- Watch; +- Download. + +--- + +## 9. Состояния скачивания + +### Не скачано + +```text +[▶ Смотреть] [⬇ Скачать] +``` + +### В очереди + +```text +[▶ Смотреть] [В очереди...] +``` + +### Скачивается + +```text +[▶ Смотреть] [64%] +``` + +Дополнительно progress-line внизу thumbnail. + +### Post-processing + +```text +[▶ Смотреть] [Обработка...] +``` + +### На сервере + +```text +[▶ Смотреть] [✓ На сервере] +``` + +### Ошибка + +```text +[▶ Смотреть] [⚠ Повторить] +``` + +Не показывать traceback. + +--- + +## 10. Video page + +Route: + +```text +/video/:youtubeVideoId +``` + +Desktop: + +```text +┌──────────────────────────────────────────────────────────────────┐ +│ │ +│ VIDEO PLAYER │ +│ │ +└──────────────────────────────────────────────────────────────────┘ + +Linux Kernel 7.0 — What's New + +The Linux Experiment +2 часа назад [⬇ Скачать] + +Источник: YouTube + +[Linux] [IT] + +──────────────────────────────────────────────────────────────────── + +Описание видео... +``` + +Если local copy существует: + +```text +Источник: ● Локальная копия · mediaVM +``` + +и вместо YouTube iframe используется HTML5 `