myYouTube/youtube_categories_metube_TZ.md

1553 lines
39 KiB
Markdown
Raw Normal View History

# Техническое задание: персональный YouTube-клиент с категориями подписок и интеграцией MeTube
**Статус:** MVP specification
**Дата:** 2026-09-16
**Режим:** single-user
**Целевая VM приложения:** `hermesVM`
**Media/download backend:** существующий MeTube на `mediaVM`
---
## 1. Цель проекта
Разработать self-hosted веб-сервис, который работает как персональная лента YouTube-подписок, но добавляет отсутствующую в YouTube удобную категоризацию каналов.
Сервис должен:
1. Авторизоваться в Google/YouTube от имени одного владельца.
2. Получать реальные подписки YouTube-аккаунта.
3. Позволять вручную распределять подписанные каналы по пользовательским категориям, например:
- Linux
- Шахматы
- Научпоп
- IT
- Игры
4. Показывать ленту последних видео:
- со всех подписок;
- из выбранной категории;
- из каналов без категории.
5. Позволять смотреть ролик:
- с YouTube, если локальной копии нет;
- напрямую с MeTube/mediaVM, если ролик был скачан.
6. Позволять нажать `Скачать`, после чего backend отправляет URL ролика в существующий MeTube.
7. Не хранить сами видео на `hermesVM`.
8. Иметь API, который в дальнейшем можно использовать для PWA/мобильного приложения.
Главная идея:
```text
YouTube = источник подписок, метаданных и online-видео
Наш сервис = каталогизация, лента, UI и бизнес-логика
MeTube = download engine + media server
```
---
## 2. Что НЕ входит в MVP
В MVP не требуется:
- воспроизводить персональные YouTube Recommendations/Home;
- копировать интерфейс youtube.com один в один;
- комментарии;
- лайки/дизлайки;
- управление подписками на YouTube;
- загрузка видео на YouTube;
- несколько пользователей;
- социальные функции;
- полноценное мобильное приложение;
- автоматическое скачивание всех новых роликов;
- функции Tube Archivist;
- перенос видеофайлов с `mediaVM` на `hermesVM`.
Позже архитектура не должна мешать добавлению PWA/мобильного приложения.
---
## 3. Проверенная инфраструктура
### 3.1. MeTube
MeTube уже работает на `mediaVM`.
Проверено:
```text
MeTube host: mediaVM
MeTube IP: 192.168.8.177
MeTube port: 8081
MeTube API base URL: http://192.168.8.177:8081
```
Docker container:
```text
name: metube
image: ghcr.io/alexta69/metube:latest
port mapping: 0.0.0.0:8081 -> 8081/tcp
```
Compose:
```text
/home/vrubel/services/meTube/compose.yml
```
Media storage на `mediaVM`:
```text
host:
/home/vrubel/hpstorage/meTube
container:
/downloads
```
### 3.2. Проверка MeTube API
С `hermesVM` успешно выполнен запрос:
```http
POST http://192.168.8.177:8081/add
Content-Type: application/json
{}
```
Ответ:
```text
HTTP/1.1 400 missing 'url', 'download_type', or 'quality'
```
Это подтверждает:
- endpoint `/add` доступен с `hermesVM`;
- MeTube сейчас не требует отдельный API token;
- сетевой маршрут `hermesVM -> mediaVM:8081` работает.
### 3.3. Проверка отдачи локального видео
Проверенный URL вида:
```text
http://192.168.8.177:8081/download/<URL-encoded-filename>
```
вернул:
```text
HTTP/1.1 200 OK
Content-Type: video/webm
Accept-Ranges: bytes
```
Следовательно, MeTube можно использовать как media server для HTML5 video playback без монтирования media storage на `hermesVM`.
---
## 4. Архитектура
```text
Google / YouTube
│
OAuth2 + Data API
│
▼
┌──────────────────────────────────────────────────────┐
│ hermesVM │
│ │
│ Browser │
│ │ │
│ ▼ │
│ Web UI ◄──────────── REST API ───────────► Backend │
│ │ │
│ ▼ │
│ PostgreSQL │
│ │ │
│ │ │
└───────────────────────────────────────────────┼──────┘
│
POST /add │
▼
┌────────────────────────┐
│ mediaVM │
│ │
│ MeTube :8081 │
│ │ │
│ ├─ yt-dlp download │
│ │ │
│ └─ /download/... │
│ │
│ /home/vrubel/ │
│ hpstorage/meTube │
└────────────────────────┘
▲
│
local video playback
│
Browser
```
### Ключевой принцип
Frontend **не должен напрямую вызывать `POST /add` MeTube**.
Правильный путь:
```text
Browser
-> our backend on hermesVM
-> MeTube API on mediaVM
```
Это позволяет:
- не раскрывать write API MeTube клиенту;
- валидировать YouTube URL;
- хранить историю download jobs;
- централизованно обрабатывать ошибки;
- позже добавить авторизацию/ACL без изменения frontend.
После завершения скачивания сам видеофайл воспроизводится **напрямую с MeTube/mediaVM**.
---
## 5. Рекомендуемый стек
### Backend
- Python 3.13+
- FastAPI
- SQLAlchemy 2.x
- Alembic
- Pydantic Settings
- httpx
- google-auth
- google-auth-oauthlib
- google-api-python-client либо прямые HTTP-вызовы к YouTube Data API
- APScheduler для простых фоновых sync jobs
- python-socketio client для получения событий MeTube
### Frontend
- React
- TypeScript
- Vite
- React Router
- TanStack Query
- обычный responsive CSS либо Tailwind CSS
Не использовать Next.js без необходимости: серверный рендеринг для этого проекта не нужен.
### Database
- PostgreSQL 16+
Redis для MVP не нужен.
### Deployment
Docker Compose на `hermesVM`.
Production может состоять всего из:
```text
app
postgres
```
Frontend можно собирать на этапе Docker build и раздавать из backend-контейнера как static assets.
---
## 6. Google OAuth и YouTube Data API
### 6.1. OAuth
Использовать OAuth 2.0 Web Application.
Минимальный scope:
```text
https://www.googleapis.com/auth/youtube.readonly
```
При необходимости идентифицировать Google account дополнительно:
```text
openid
email
profile
```
Приложение строго однопользовательское.
В `.env` должен быть параметр:
```env
ALLOWED_GOOGLE_EMAIL=user@example.com
```
После OAuth backend обязан отклонить другой аккаунт.
Refresh token должен храниться зашифрованно.
В репозиторий нельзя коммитить:
- Google Client Secret;
- refresh token;
- session secret;
- encryption key.
---
## 7. Получение подписок
Для получения подписок авторизованного пользователя использовать:
```http
GET https://www.googleapis.com/youtube/v3/subscriptions
```
Параметры:
```text
part=snippet,contentDetails
mine=true
maxResults=50
```
Обязательно обрабатывать `nextPageToken`.
Для каждого subscription сохранить минимум:
```text
youtube_channel_id
title
description
thumbnail_url
subscribed=true
```
После полного sync канал, который был в БД, но исчез из актуального списка подписок, не удалять физически, а установить:
```text
subscribed=false
```
Это сохранит пользовательские категории и историю.
---
## 8. Получение uploads playlist каналов
После sync подписок получить channel metadata через:
```http
GET https://www.googleapis.com/youtube/v3/channels
```
Использовать:
```text
part=snippet,contentDetails
id=<channel ids>
```
Channel IDs следует отправлять batch-запросами, а не по одному.
Из:
```text
contentDetails.relatedPlaylists.uploads
```
сохранить:
```text
uploads_playlist_id
```
для каждого канала.
---
## 9. Синхронизация новых видео
Для каждого активного подписанного канала читать его uploads playlist через:
```http
GET https://www.googleapis.com/youtube/v3/playlistItems
```
Параметры:
```text
part=snippet,contentDetails
playlistId=<uploads_playlist_id>
maxResults=10
```
Затем новые `video_id` группировать и получать детали через:
```http
GET https://www.googleapis.com/youtube/v3/videos
```
Например:
```text
part=snippet,contentDetails,status
id=<up to batch limit>
```
Хранить локальный cache метаданных.
### Default sync intervals
```env
SUBSCRIPTIONS_SYNC_INTERVAL_HOURS=6
VIDEOS_SYNC_INTERVAL_MINUTES=60
VIDEOS_PER_CHANNEL_SYNC=10
```
Также нужна кнопка:
```text
Обновить сейчас
```
Нельзя при каждом открытии frontend делать запросы YouTube отдельно для каждого канала.
UI должен читать прежде всего нашу локальную БД.
---
## 10. YouTube API quota
YouTube Data API имеет quota.
Default quota проекта обычно составляет 10 000 units/day, но это значение следует считать конфигурируемым/проверяемым в Google Cloud Console.
Нужно:
- минимизировать повторные вызовы;
- использовать локальный cache;
- batch-ить `channels.list` и `videos.list`;
- не запускать новый полный sync одновременно с уже идущим;
- логировать quota-related errors;
- показывать UI сообщение при quota exhaustion;
- не превращать пользовательский refresh страницы в полный YouTube sync.
---
## 11. Модель данных
### `app_settings`
```text
id
key
value
updated_at
```
### `oauth_credentials`
Single row.
```text
id
google_email
encrypted_refresh_token
access_token_expires_at
created_at
updated_at
```
Access token можно не сохранять постоянно, если библиотека умеет обновлять его через refresh token.
### `channels`
```text
id UUID/BigInt
youtube_channel_id UNIQUE NOT NULL
title
description
thumbnail_url
uploads_playlist_id
subscribed BOOLEAN
last_synced_at
created_at
updated_at
```
### `categories`
```text
id
name UNIQUE
slug UNIQUE
sort_order
created_at
updated_at
```
### `channel_categories`
Many-to-many:
```text
channel_id
category_id
PRIMARY KEY(channel_id, category_id)
```
Один канал может входить в несколько категорий.
### `videos`
```text
id
youtube_video_id UNIQUE NOT NULL
channel_id
title
description
thumbnail_url
published_at
duration_seconds NULL
youtube_url
created_at
updated_at
```
### `download_jobs`
```text
id
video_id
status
metube_job_id NULL
metube_filename NULL
media_url NULL
progress_percent NULL
error_message NULL
requested_at
started_at NULL
completed_at NULL
updated_at
```
`status` enum:
```text
queued
downloading
postprocessing
completed
failed
unknown
```
---
## 12. Категории
UI обязан иметь виртуальные категории:
```text
Все
Без категории
```
Они не обязаны храниться в таблице `categories`.
Пользовательские категории:
- создать;
- переименовать;
- удалить;
- изменить порядок;
- назначить каналу;
- убрать канал из категории.
Удаление категории **не должно удалять канал**.
Канал может одновременно находиться, например, в:
```text
Linux
IT
Self-hosted
```
---
## 13. Основная лента
Главная страница:
```text
[Все] [Linux] [Шахматы] [Научпоп] ...
```
или sidebar на desktop.
После выбора категории показывать последние ролики всех каналов этой категории.
Сортировка:
```text
published_at DESC
```
Карточка ролика должна содержать минимум:
- thumbnail;
- title;
- channel title;
- publish date/time;
- duration, если известна;
- local/download status;
- кнопку `Смотреть`;
- кнопку `Скачать`, если локальной копии нет.
Желательные фильтры после базового MVP:
- только новые;
- только локальные;
- только нескачанные;
- скрыть просмотренные;
- Shorts.
Эти фильтры не являются blocker для первой версии.
---
## 14. Просмотр видео
### 14.1. Если локальной копии нет
Использовать YouTube player/embed.
Например:
```text
https://www.youtube.com/embed/<VIDEO_ID>
```
или privacy-enhanced:
```text
https://www.youtube-nocookie.com/embed/<VIDEO_ID>
```
### 14.2. Если локальная копия есть
Использовать обычный HTML5:
```html
<video controls src="<media_url>"></video>
```
`media_url` должен указывать напрямую на MeTube/mediaVM.
Проверено, что MeTube отвечает:
```text
Accept-Ranges: bytes
```
поэтому browser seeking должен работать.
### 14.3. Fallback
Если `media_url` сохранён в БД, но MeTube отвечает `404/5xx`, UI не должен ломаться.
Нужно:
1. пометить local state как stale/unknown;
2. предложить YouTube playback;
3. дать возможность скачать заново.
---
## 15. Интеграция с MeTube
### 15.1. Конфигурация
Backend config:
```env
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
```
Разделять:
```text
METUBE_API_BASE_URL
```
и:
```text
METUBE_PUBLIC_BASE_URL
```
обязательно.
Причина: позже internal API может остаться HTTP/IP, а media playback перейти на HTTPS domain через Nginx Proxy Manager.
### Важное замечание по HTTPS
Если наше приложение позже будет открываться через:
```text
https://...
```
браузер не должен получать video URL вида:
```text
http://192.168.8.177:8081/...
```
из-за mixed-content policy.
Тогда `METUBE_PUBLIC_BASE_URL` должен стать HTTPS URL, например через reverse proxy.
Внутренний `METUBE_API_BASE_URL` при этом может остаться:
```text
http://192.168.8.177:8081
```
---
## 16. Добавление загрузки в MeTube
Использовать:
```http
POST /add
```
Полный URL:
```text
http://192.168.8.177:8081/add
```
Пример payload для MVP:
```json
{
"url": "https://www.youtube.com/watch?v=VIDEO_ID",
"download_type": "video",
"codec": "auto",
"format": "mp4",
"quality": "best",
"auto_start": true,
"custom_name_prefix": "VIDEO_ID"
}
```
`url`, `download_type` и `quality` обязательны в текущем API MeTube.
`format=mp4` выбран для более предсказуемой browser compatibility.
`custom_name_prefix` рекомендуется устанавливать равным YouTube Video ID. Это значительно упрощает correlation между нашим `video` и событиями MeTube и не требует менять глобальный `OUTPUT_TEMPLATE` существующего MeTube.
Backend должен:
1. проверить, что `youtube_video_id` существует в нашей БД;
2. построить canonical URL:
```text
https://www.youtube.com/watch?v=<VIDEO_ID>
```
3. не принимать произвольный URL из frontend;
4. отправить POST в MeTube;
5. сохранить возвращённый MeTube identifier/status, если он присутствует;
6. создать/обновить `download_jobs`.
Повторное нажатие `Скачать` не должно создавать несколько одинаковых активных jobs.
---
## 17. Получение статуса MeTube download
Текущий MeTube использует Socket.IO и публикует события, включая:
```text
added
updated
completed
canceled
cleared
```
Наш backend должен держать server-to-server Socket.IO connection к MeTube.
Никакой Socket.IO MeTube напрямую в browser нашего приложения не нужен.
При событиях:
```text
added/updated
```
обновлять:
```text
status
progress_percent
```
При:
```text
completed
```
сохранить точное поле `filename`, пришедшее от MeTube.
Это важно: нельзя пытаться восстановить имя файла по YouTube title.
---
## 18. Построение media URL
При `completed` MeTube сообщает финальное имя/путь файла.
Если MeTube вернул:
```text
/downloads/some file.mp4
```
backend должен:
1. безопасно проверить, что путь находится под:
```text
METUBE_CONTAINER_DOWNLOAD_DIR=/downloads
```
2. получить relative path:
```text
some file.mp4
```
3. URL-encode каждый path segment;
4. сформировать:
```text
<METUBE_PUBLIC_BASE_URL>/download/<encoded-relative-path>
```
Пример:
```text
http://192.168.8.177:8081/download/some%20file.mp4
```
5. выполнить `HEAD` перед переводом job в окончательный `completed`, если это не создаёт проблем с конкретной версией MeTube;
6. сохранить media URL в БД.
Нельзя строить путь на основе title из YouTube API: yt-dlp/MeTube могут санитизировать, сокращать или менять filename.
---
## 19. Recovery при рестарте приложения
Backend не должен считать download завершённым только потому, что когда-то отправил `/add`.
После рестарта:
- `completed` jobs с рабочим `media_url` остаются completed;
- media URL можно выборочно проверять HEAD-запросом;
- незавершённые `queued/downloading/postprocessing` jobs переводятся в `unknown`, если их состояние невозможно достоверно восстановить;
- если текущая версия MeTube позволяет получить persistent queue/completed state через Socket.IO/API, использовать это для reconciliation;
- если нет — UI должен показывать `Состояние неизвестно` и позволять повторную проверку/загрузку.
Не вносить изменения в исходники MeTube ради этого MVP.
---
## 20. Наш backend REST API
Минимальный API:
### Health
```http
GET /api/health
```
Ответ:
```json
{
"status": "ok",
"database": "ok",
"metube": "ok"
}
```
Google API не обязательно проверять на каждом healthcheck.
### Auth
```http
GET /api/auth/status
GET /api/auth/google/start
GET /api/auth/google/callback
POST /api/auth/logout
```
### Sync
```http
POST /api/sync/subscriptions
POST /api/sync/videos
GET /api/sync/status
```
Запретить параллельные одинаковые sync jobs.
### Categories
```http
GET /api/categories
POST /api/categories
PATCH /api/categories/{id}
DELETE /api/categories/{id}
POST /api/categories/reorder
```
### Channels
```http
GET /api/channels
GET /api/channels/{id}
PUT /api/channels/{id}/categories
```
Опциональные query params:
```text
subscribed=true
category_id=<id>
uncategorized=true
```
### Feed
```http
GET /api/feed
```
Query params:
```text
category_id
limit
cursor
local_only
downloaded
```
Для `Все` category_id не передавать.
### Videos
```http
GET /api/videos/{youtube_video_id}
POST /api/videos/{youtube_video_id}/download
GET /api/videos/{youtube_video_id}/download-status
POST /api/videos/{youtube_video_id}/recheck-local
```
---
## 21. Pagination
Ленту не возвращать целиком.
Использовать cursor pagination.
Сортировка:
```text
published_at DESC, id DESC
```
Default:
```text
limit=30
```
Maximum:
```text
limit=100
```
---
## 22. Frontend screens
### 22.1. First-run / Connect YouTube
Если OAuth ещё не выполнен:
```text
Подключить YouTube
```
После успешного OAuth автоматически запустить initial subscriptions sync.
### 22.2. Feed
Основной экран.
Desktop:
```text
sidebar categories + video grid/feed
```
Mobile:
```text
top horizontal category selector / drawer
```
### 22.3. Channels
Страница всех подписок.
Для каждого канала:
- avatar;
- title;
- category chips;
- возможность добавить/убрать категории.
Нужен поиск по названию канала.
### 22.4. Category management
- create;
- rename;
- delete;
- reorder.
### 22.5. Video view
Показывать player.
Логика выбора source:
```text
download_job.completed + media_url reachable
-> MeTube local player
else
-> YouTube player
```
---
## 23. Download UX
Карточка должна показывать состояния примерно так:
```text
Скачать
↓
В очереди
↓
Скачивается 42%
↓
Обработка
↓
На сервере
```
При ошибке:
```text
Ошибка загрузки
[Повторить]
```
После completed кнопка меняется на:
```text
На сервере
```
Можно добавить отдельную ссылку:
```text
Открыть файл
```
---
## 24. Безопасность
### MeTube
Сейчас MeTube API доступен без token authentication.
Поэтому:
- не публиковать `mediaVM:8081` напрямую в интернет без reverse proxy/auth;
- наш browser не должен иметь возможность отправлять download POST напрямую;
- write calls к MeTube идут только с backend;
- по возможности firewall должен разрешать MeTube API только из доверенных LAN/VLAN адресов.
### Наш сервис
Нужно:
- single-user access;
- secure session cookie;
- `HttpOnly`;
- `SameSite=Lax` или строже;
- `Secure=true` при HTTPS;
- OAuth state validation;
- CSRF protection для mutating browser requests или эквивалентная same-origin защита;
- allow-list Google email;
- secrets только через environment/secrets;
- никакого логирования OAuth tokens;
- URL validation;
- timeout на запросы к Google/MeTube;
- ограниченный размер request body;
- понятные error messages без stack trace пользователю.
---
## 25. `.env.example`
Пример:
```env
APP_ENV=production
APP_BASE_URL=http://hermesVM:8080
APP_PORT=8080
APP_SECRET_KEY=CHANGE_ME
TOKEN_ENCRYPTION_KEY=CHANGE_ME
DATABASE_URL=postgresql+psycopg://youtube_app:CHANGE_ME@postgres:5432/youtube_app
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://hermesVM:8080/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
```
Не хардкодить секреты или Google account в source code.
---
## 26. Docker Compose на hermesVM
Ожидаемая структура:
```text
youtube-app/
├── backend/
├── frontend/
├── migrations/
├── tests/
├── Dockerfile
├── compose.yml
├── .env
├── .env.example
├── README.md
└── AGENTS.md
```
Production compose примерно:
```text
services:
app:
build: .
restart: unless-stopped
env_file:
- .env
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: youtube_app
POSTGRES_USER: youtube_app
POSTGRES_PASSWORD: ...
volumes:
- postgres_data:/var/lib/postgresql/data
```
Никаких media volumes на `hermesVM` не требуется.
---
## 27. Healthchecks
### App
```text
GET /api/health
```
Docker healthcheck должен использовать его.
### PostgreSQL
`pg_isready`.
### MeTube
Backend health может выполнить недорогой HTTP request на MeTube.
Нельзя считать весь app `unhealthy`, если Google API временно недоступен: это external dependency.
---
## 28. Logging
Структурированные или как минимум единообразные logs.
Логировать:
- app startup/shutdown;
- OAuth login без token values;
- subscription sync start/end;
- video sync start/end;
- количество полученных/обновлённых объектов;
- YouTube quota/rate errors;
- MeTube request/result;
- MeTube event state changes;
- download failures;
- DB migration version.
Не логировать:
- access token;
- refresh token;
- client secret;
- session secret.
---
## 29. Error handling
UI должен различать:
```text
YouTube API temporarily unavailable
YouTube quota exhausted
OAuth expired/revoked
MeTube unavailable
MeTube rejected download
Download failed
Local media missing
Database error
```
Не показывать пользователю generic blank page.
---
## 30. Migration strategy
Использовать Alembic с первого commit.
Приложение не должно создавать/изменять schema ad-hoc при старте без migrations.
Команда production migration должна быть документирована.
Например:
```text
alembic upgrade head
```
Можно выполнять migration из entrypoint перед запуском app, если используется advisory lock/безопасная single-instance схема.
---
## 31. Tests
Минимум:
### Backend unit tests
- category CRUD;
- assigning channel to multiple categories;
- feed filtering;
- pagination;
- OAuth allowed-email check;
- Google API pagination;
- sync idempotency;
- MeTube payload generation;
- duplicate download protection;
- filename -> media URL encoding;
- local media fallback.
### Integration tests
Использовать mocked Google API и mocked MeTube.
Отдельный opt-in test допускается против реального:
```text
http://192.168.8.177:8081
```
но он не должен запускаться в обычном CI.
### Frontend
Проверить минимум:
- category switch;
- cards rendering;
- download state transitions;
- local/YouTube player selection;
- error state.
---
## 32. MVP acceptance criteria
MVP считается готовым, когда выполняются все пункты:
1. Пользователь открывает сервис на `hermesVM`.
2. Подключает один разрешённый Google account.
3. Сервис импортирует все доступные YouTube subscriptions.
4. Список каналов отображается с thumbnails и названиями.
5. Можно создать категорию `Linux`.
6. Можно назначить в неё несколько каналов.
7. При открытии `Linux` отображаются свежие видео только этих каналов.
8. `Все` отображает свежие видео всех подписанных каналов.
9. Feed работает из нашей БД и не вызывает полный YouTube sync на каждый page load.
10. Нескачанное видео можно посмотреть через YouTube.
11. На карточке работает `Скачать`.
12. Backend отправляет корректный POST в:
`http://192.168.8.177:8081/add`.
13. Download отображает хотя бы состояния `queued/downloading/completed/failed`.
14. После completion точный filename сохраняется в БД.
15. Сервис строит корректный MeTube `/download/...` URL.
16. Скачанное видео проигрывается напрямую с `mediaVM`.
17. Seeking внутри локального видео работает.
18. При исчезновении локального файла UI корректно fallback-ится на YouTube.
19. Приложение переживает restart Docker containers без потери categories и cached feed.
20. Секреты отсутствуют в git.
21. Проект имеет README с инструкцией запуска.
22. `docker compose up -d` является основным способом production deployment на `hermesVM`.
---
## 33. Приоритет реализации
Рекомендуемый порядок для coding agent:
### Phase 1 — Skeleton
- FastAPI;
- PostgreSQL;
- Alembic;
- React;
- Docker Compose;
- healthcheck.
### Phase 2 — Google OAuth + subscriptions
- OAuth;
- subscriptions sync;
- channels table;
- channels UI.
### Phase 3 — Categories
- CRUD;
- many-to-many;
- category filter.
### Phase 4 — Video sync/feed
- uploads playlists;
- playlistItems;
- videos metadata;
- cached feed;
- background scheduler.
### Phase 5 — YouTube playback
- video detail page/modal;
- YouTube embed.
### Phase 6 — MeTube
- `/add`;
- download_jobs;
- Socket.IO consumer;
- progress;
- exact filename;
- media URL;
- local playback.
### Phase 7 — Hardening
- retry/recovery;
- logging;
- quotas;
- tests;
- README;
- HTTPS/reverse-proxy notes.
Не реализовывать последующие фазы заранее, если предыдущая не работает.
---
## 34. Важные архитектурные ограничения для агента
1. **Не форкать Tube Archivist.**
2. **Не встраивать yt-dlp в наш сервис.**
3. **Не дублировать функции скачивания MeTube.**
4. **Не монтировать media storage на hermesVM.**
5. **Не получать YouTube recommendations.**
6. **Не делать multi-user architecture в MVP.**
7. **Не привязывать frontend напрямую к Google API.**
8. **Не привязывать frontend напрямую к MeTube write API.**
9. **Не определять локальный filename из YouTube title.**
10. **Не удалять существующие MeTube-файлы.**
11. **Не менять исходный код MeTube.**
12. Все внешние base URLs должны задаваться через config/env.
13. Backend REST API должен быть пригоден для будущего mobile client.
---
## 35. Возможные улучшения после MVP
Не включать в первую реализацию без отдельного решения:
- PWA;
- Android/iOS app;
- просмотрено/не просмотрено;
- Watch Later внутри нашего приложения;
- автоматическое удаление локальной копии через N дней;
- pin/keep forever;
- поиск по подпискам;
- полнотекстовый поиск по videos;
- фильтр Shorts;
- notification о новых видео в категории;
- автоматическое скачивание выбранных каналов;
- SponsorBlock при скачивании;
- выбор качества перед download;
- subtitles;
- отдельный TV UI;
- Chromecast;
- импорт существующих скачанных MeTube файлов;
- offline metadata snapshots.
---
## 36. Известные риски
### YouTube API quota
При большом числе подписок частый polling может расходовать quota. Интервалы должны быть configurable.
### YouTube/yt-dlp changes
MeTube зависит от yt-dlp, поэтому загрузка YouTube может периодически ломаться после изменений YouTube. Наш сервис не должен пытаться самостоятельно чинить extraction layer.
### MeTube API stability
`POST /add` и Socket.IO относятся к API существующего сервиса, но это не формально versioned enterprise API. Интеграцию вынести в отдельный класс:
```text
MeTubeClient
```
Например:
```python
class MeTubeClient:
async def enqueue_video(...)
async def health(...)
async def check_media(...)
async def run_event_listener(...)
```
Нельзя размазывать MeTube-specific HTTP calls по всему backend.
### Mixed content
Если frontend станет HTTPS, local video тоже должен идти по HTTPS.
### Existing MeTube downloads
MVP обязан надёжно отслеживать загрузки, инициированные нашим сервисом.
Автоматическое распознавание всех старых файлов, ранее скачанных вручную через MeTube, в MVP не требуется.
---
## 37. Источники/документация
YouTube Data API:
- https://developers.google.com/youtube/v3/docs/subscriptions/list
- https://developers.google.com/youtube/v3/docs/channels
- https://developers.google.com/youtube/v3/docs/playlistItems/list
- https://developers.google.com/youtube/v3/docs/videos/list
- https://developers.google.com/youtube/v3/getting-started
MeTube:
- https://github.com/alexta69/metube
- https://github.com/alexta69/metube/blob/master/README.md
- https://github.com/alexta69/metube/blob/master/app/main.py
- https://github.com/alexta69/metube/blob/master/app/ytdl.py
---
# Итоговая формулировка для coding agent
Нужно создать single-user self-hosted приложение на `hermesVM`, которое через Google OAuth читает реальные подписки YouTube пользователя, сохраняет их в PostgreSQL, позволяет распределять каналы по пользовательским категориям и показывает cached chronological feed последних видео по всем подпискам или выбранной категории.
Видео, которых нет локально, проигрываются через YouTube embed.
По кнопке `Скачать` backend отправляет video URL в существующий MeTube на `mediaVM` (`http://192.168.8.177:8081/add`), отслеживает состояние загрузки, получает точный filename после завершения и сохраняет media URL. После этого видео должно проигрываться напрямую с MeTube `/download/...`, а не с YouTube и не через storage `hermesVM`.
Не реализовывать рекомендации YouTube, multi-user, собственный yt-dlp downloader или Tube Archivist.