1553 lines
39 KiB
Markdown
1553 lines
39 KiB
Markdown
|
|
# Техническое задание: персональный 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.
|