Translate documentation to english

This commit is contained in:
heboba
2026-02-26 20:25:20 +00:00
parent 06add127ff
commit bd23a9da8a
2 changed files with 341 additions and 338 deletions

332
DESIGN.md
View File

@@ -1,86 +1,86 @@
# Design / Architecture # Design / Architecture
## Цель ## Goal
Сервис генерирует Spotify playlist "daily vibe" на основе: The service generates a Spotify "daily vibe" playlist based on:
- recent listening пользователя - the user's recent listening
- кэша liked tracks - a local cache of liked tracks
- истории уже выданных рекомендаций - the history of tracks previously recommended by the bot
Управление идет через Telegram-бота (`/generate`, `/connect`, `/status` и т.д.), а также опционально через nightly cron trigger. The main user interface is a Telegram bot (`/generate`, `/connect`, `/status`, etc.), with an optional nightly cron trigger.
## Высокоуровневая схема ## High-level overview
Компоненты: Core components:
- `FastAPI` приложение - `FastAPI` application
- health check - health check
- Spotify OAuth start/callback - Spotify OAuth start/callback
- internal endpoint для cron (`/internal/jobs/nightly`) - internal endpoint for cron (`/internal/jobs/nightly`)
- `TelegramBotRunner` (polling) - `TelegramBotRunner` (polling)
- принимает команды пользователей - handles user commands
- запускает генерацию и отправляет статусы - starts generation and sends status updates
- `PlaylistJobService` - `PlaylistJobService`
- orchestration одного run (token -> sync likes -> candidates -> playlist -> persist) - orchestrates a single run (token -> sync likes -> candidates -> playlist -> persist)
- `RecommendationEngine` - `RecommendationEngine`
- строит seed profile - builds seed profile
- собирает candidate pool - collects candidate pool
- ранжирует и отбирает треки - ranks and selects tracks
- `SpotifyClient` / `LastFmClient` - `SpotifyClient` / `LastFmClient`
- внешние API вызовы - external API calls
- `SQLite` (через SQLAlchemy async) - `SQLite` (via async SQLAlchemy)
- пользователи, кэш лайков, история рекомендаций, run log - users, liked cache, recommendation history, run log
## Runtime / Lifecycle ## Runtime / lifecycle
Точка входа: `app/main.py`. Entry point: `app/main.py`.
На startup: On startup:
1. Загружается `Settings` (`app/config.py`) 1. Load `Settings` (`app/config.py`)
2. Создается async SQLAlchemy engine и session factory (`app/db/session.py`) 2. Create async SQLAlchemy engine and session factory (`app/db/session.py`)
3. Выполняется `create_all` (автосоздание таблиц) 3. Run `create_all` (auto-create tables)
4. Создается общий `httpx.AsyncClient` 4. Create shared `httpx.AsyncClient`
5. Создаются клиенты: 5. Create API clients:
- `SpotifyClient` - `SpotifyClient`
- `LastFmClient` - `LastFmClient`
6. Создаются сервисы: 6. Create services:
- `SpotifyAuthService` - `SpotifyAuthService`
- `RecommendationEngine` - `RecommendationEngine`
- `PlaylistJobService` - `PlaylistJobService`
7. Инициализируется `TelegramBotRunner` и запускается polling 7. Initialize `TelegramBotRunner` and start polling
8. Все объекты складываются в `app.state.runtime` и `app.state.services` 8. Store runtime/service objects in `app.state.runtime` and `app.state.services`
На shutdown: On shutdown:
- останавливается Telegram polling - stop Telegram polling
- закрывается `httpx.AsyncClient` - close `httpx.AsyncClient`
- закрывается DB engine - dispose DB engine
## Контейнеры / Deployment ## Containers / deployment
`docker-compose.yml`: `docker-compose.yml` defines:
- `app` (основной сервис, FastAPI + Telegram polling) - `app` (main service, FastAPI + Telegram polling)
- `cron` (опциональный сервис с `supercronic`) - `cron` (optional service with `supercronic`)
Важно: Important:
- `cron` помечен `profiles: ["cron"]` и по умолчанию не стартует - `cron` is under `profiles: ["cron"]` and does not start by default
- manual-first режим: пользователь генерирует плейлисты через Telegram `/generate` - the project is now manual-first: users generate playlists via Telegram `/generate`
`cron` выполняет `scripts/run_nightly.sh`, который вызывает: `cron` runs `scripts/run_nightly.sh`, which calls:
- `POST /internal/jobs/nightly` с `Authorization: Bearer <INTERNAL_JOB_TOKEN>` - `POST /internal/jobs/nightly` with `Authorization: Bearer <INTERNAL_JOB_TOKEN>`
## Слои приложения ## Application layers
### 1. API Layer (`app/api/routes.py`) ### 1. API layer (`app/api/routes.py`)
Назначение: Responsibilities:
- HTTP endpoints для OAuth и internal jobs - HTTP endpoints for OAuth and internal jobs
Endpoints: Endpoints:
@@ -89,18 +89,18 @@ Endpoints:
- `GET /auth/spotify/callback` - `GET /auth/spotify/callback`
- `POST /internal/jobs/nightly` - `POST /internal/jobs/nightly`
Особенности: Notes:
- OAuth callback после успеха отправляет сообщение в Telegram пользователю - OAuth callback sends a Telegram notification to the user on success
- internal nightly endpoint защищен `INTERNAL_JOB_TOKEN` - nightly endpoint is protected by `INTERNAL_JOB_TOKEN`
### 2. Bot Layer (`app/bot/telegram_bot.py`) ### 2. Bot layer (`app/bot/telegram_bot.py`)
Назначение: Responsibilities:
- пользовательский интерфейс через Telegram команды - user-facing interface via Telegram commands and reply-keyboard buttons
Поддерживаемые команды: Supported commands:
- `/start` - `/start`
- `/help` - `/help`
@@ -111,118 +111,120 @@ Endpoints:
- `/setsize` - `/setsize`
- `/setratio` - `/setratio`
- `/sync` - `/sync`
- `/lang`
Особенности: Notes:
- `/generate` запускает `PlaylistJobService.generate_for_user(..., force=True, notify=False)` - `/generate` calls `PlaylistJobService.generate_for_user(..., force=True, notify=False)`
- `/sync` только обновляет кэш лайков - `/sync` only refreshes liked tracks cache
- у каждой команды свой короткий DB session через `session_factory` - each command uses a short-lived DB session from `session_factory`
- bot UI supports `ru` and `en` (localized text/buttons)
### 3. Service Layer ### 3. Service layer
#### `SpotifyAuthService` (`app/services/spotify_auth.py`) #### `SpotifyAuthService` (`app/services/spotify_auth.py`)
Роли: Responsibilities:
- создание OAuth state - create OAuth state
- обмен `code` на токены - exchange `code` for tokens
- refresh access token - refresh access token
- защита от истекшего access token - ensure valid access token before Spotify calls
Особенности: Notes:
- сравнение дат нормализуется в UTC (важно для SQLite naive datetime) - datetime comparison is normalized to UTC (important for SQLite naive datetimes)
- сохраняет scopes и expiry в таблице `users` - stores scopes and expiry on the `users` row
#### `RecommendationEngine` (`app/services/recommendation.py`) #### `RecommendationEngine` (`app/services/recommendation.py`)
Роли: Responsibilities:
- sync liked tracks в локальный кэш - sync liked tracks into local cache
- build seed profile - build seed profile
- collect candidates из нескольких источников - collect candidates from multiple sources
- rank/select итоговый список - rank/select final track list
Текущие источники кандидатов: Current candidate sources:
- Spotify recommendations - Spotify recommendations
- Spotify artist top tracks - Spotify artist top tracks
- Spotify search (seed artist fallback) - Spotify search (seed-artist fallback)
- Last.fm track similar -> Spotify search - Last.fm track similar -> Spotify search
- Last.fm artist similar -> Spotify search - Last.fm artist similar -> Spotify search
Ключевые особенности: Key implementation details:
- соблюдение лимита Spotify recommendations: максимум `5` seed'ов на запрос - respects Spotify recommendations seed limit: max `5` seeds per request
- мягкая деградация при частичных ошибках источников - degrades gracefully when some sources fail
- liked fallback (если весь пул оказался уже в лайках) - includes liked fallback (if all candidates are already liked)
#### `PlaylistJobService` (`app/services/playlist_job.py`) #### `PlaylistJobService` (`app/services/playlist_job.py`)
Роли: Responsibilities:
- orchestration полного run - orchestrate an end-to-end playlist generation run
- создание Spotify playlist и добавление треков - create Spotify playlist and add tracks
- запись run и треков в БД - persist run details and track list
- обновление recommendation history - update recommendation history
- отправка уведомления в Telegram (если задан notifier) - send Telegram notifications (if notifier is configured)
Порядок выполнения run: Run sequence:
1. Проверка пользователя / Spotify connection 1. Validate user / Spotify connection
2. Создание записи `playlist_runs` со статусом `running` 2. Create `playlist_runs` row with `running` status
3. Получение valid access token 3. Get valid access token
4. Sync liked tracks 4. Sync liked tracks
5. Сборка плейлиста через `RecommendationEngine` 5. Build playlist via `RecommendationEngine`
6. Создание playlist в Spotify 6. Create playlist in Spotify
7. Добавление треков в playlist 7. Add tracks to playlist
8. Сохранение run-трека/истории/метаданных 8. Persist run tracks / history / metadata
9. Commit и возврат `JobOutcome` 9. Commit and return `JobOutcome`
При ошибке: On error:
- `playlist_runs.status = failed` - `playlist_runs.status = failed`
- в `notes` записывается сообщение ошибки - error message is written to `notes`
## Client Layer ## Client layer
### `SpotifyClient` (`app/clients/spotify.py`) ### `SpotifyClient` (`app/clients/spotify.py`)
Инкапсулирует Spotify Web API. Encapsulates Spotify Web API calls.
Что важно в текущей реализации: Important implementation choices:
- `create_playlist()` использует `POST /me/playlists` - `create_playlist()` uses `POST /me/playlists`
- это выбранный маршрут, потому что `POST /users/{id}/playlists` может давать `403` в некоторых аккаунтах/приложениях - chosen because `POST /users/{id}/playlists` can return `403` in some app/account combinations
- `add_playlist_items()` использует `POST /playlists/{playlist_id}/items` - `add_playlist_items()` uses `POST /playlists/{playlist_id}/items`
- маршрут `/tracks` в практике может отдавать `403`, хотя `/items` работает - `/tracks` may return `403` while `/items` succeeds
- `delete_playlist()` вызывает `DELETE /playlists/{playlist_id}/followers` - `delete_playlist()` uses `DELETE /playlists/{playlist_id}/followers`
- это "unfollow" (Spotify не поддерживает hard delete playlist) - this is "unfollow" (Spotify does not support hard-delete of playlists)
- встроены retry на `429` (rate-limit) с `Retry-After` - built-in retry for `429` rate limiting using `Retry-After`
### `LastFmClient` (`app/clients/lastfm.py`) ### `LastFmClient` (`app/clients/lastfm.py`)
Используется как optional enrichment layer. Optional enrichment source for similarity.
- может быть отключен (если `LASTFM_API_KEY` пустой) - can be disabled (empty `LASTFM_API_KEY`)
- ошибки Last.fm не должны ломать весь run, если другие источники работают - Last.fm errors should not fail the whole run if other sources still work
## Persistence Layer (SQLite + SQLAlchemy) ## Persistence layer (SQLite + SQLAlchemy)
### Таблицы (`app/db/models.py`) ### Tables (`app/db/models.py`)
#### `users` #### `users`
Хранит: Stores:
- Telegram identity (`telegram_chat_id`, `telegram_username`) - Telegram identity (`telegram_chat_id`, `telegram_username`)
- Spotify identity/tokens/scopes (`spotify_user_id`, access/refresh token, expiry, scopes) - Spotify identity/tokens/scopes (`spotify_user_id`, access/refresh token, expiry, scopes)
- пользовательские настройки (`playlist_size`, `min_new_ratio`, timezone) - user settings (`playlist_size`, `min_new_ratio`, timezone)
- последние результаты (`last_generated_date`, `latest_playlist_id`, `latest_playlist_url`) - last outputs (`last_generated_date`, `latest_playlist_id`, `latest_playlist_url`)
#### `auth_states` #### `auth_states`
Временные OAuth state для callback: Temporary OAuth state for callback:
- `state` - `state`
- `telegram_chat_id` - `telegram_chat_id`
@@ -230,15 +232,15 @@ Endpoints:
#### `saved_tracks` #### `saved_tracks`
Локальный кэш `Liked Songs` пользователя: Local cache of the user's `Liked Songs`:
- `spotify_track_id` - `spotify_track_id`
- название/артисты/album/popularity - track/artist metadata, album, popularity
- `added_at` - `added_at`
#### `recommendation_history` #### `recommendation_history`
История ранее рекомендованных треков: History of previously recommended tracks:
- `spotify_track_id` - `spotify_track_id`
- `first_recommended_at` - `first_recommended_at`
@@ -247,30 +249,30 @@ Endpoints:
#### `playlist_runs` #### `playlist_runs`
Run log генерации: Playlist generation run log:
- статус (`running/success/failed`) - status (`running/success/failed`)
- метаданные Spotify playlist - Spotify playlist metadata
- статистика (`total/new/reused`) - stats (`total/new/reused`)
- `notes` - `notes`
#### `playlist_run_tracks` #### `playlist_run_tracks`
Снимок состава конкретного run: Snapshot of tracks in a specific run:
- track id / name / artists - track id / name / artists
- source (из какого источника пришел) - source (which source produced the track)
- позиция - position
- `is_new_to_bot` - `is_new_to_bot`
### Repository Layer (`app/db/repositories.py`) ### Repository layer (`app/db/repositories.py`)
Паттерн: Pattern:
- thin repositories над SQLAlchemy AsyncSession - thin repositories over `AsyncSession`
- изолируют CRUD/query-логику от service layer - isolates CRUD/query logic from the service layer
Примеры: Repositories include:
- `UserRepository` - `UserRepository`
- `AuthStateRepository` - `AuthStateRepository`
@@ -278,55 +280,55 @@ Run log генерации:
- `RecommendationHistoryRepository` - `RecommendationHistoryRepository`
- `PlaylistRunRepository` - `PlaylistRunRepository`
## Потоки данных ## Data flows
### OAuth Flow ### OAuth flow
1. Telegram `/connect` 1. Telegram `/connect`
2. `SpotifyAuthService.create_connect_url()` 2. `SpotifyAuthService.create_connect_url()`
3. Пользователь идет в Spotify auth page 3. User opens Spotify auth page
4. `GET /auth/spotify/callback` 4. `GET /auth/spotify/callback`
5. `SpotifyAuthService.handle_callback()` 5. `SpotifyAuthService.handle_callback()`
6. Токены и Spotify profile сохраняются в `users` 6. Tokens and Spotify profile are saved to `users`
7. Пользователю отправляется сообщение в Telegram 7. User receives a Telegram confirmation message
### Manual Generate Flow (`/generate`) ### Manual generation flow (`/generate`)
1. Telegram `/generate` 1. Telegram `/generate`
2. `PlaylistJobService.generate_for_user(..., force=True)` 2. `PlaylistJobService.generate_for_user(..., force=True)`
3. Sync likes + recent listening + candidate collection 3. Sync likes + load recent listening + collect candidates
4. Playlist create + add items в Spotify 4. Create playlist + add items in Spotify
5. Persist run/history 5. Persist run/history
6. Ответ пользователю в Telegram 6. Reply to user in Telegram
### Nightly Cron Flow (optional) ### Nightly cron flow (optional)
1. `supercronic` в `cron` контейнере 1. `supercronic` in the `cron` container
2. `scripts/run_nightly.sh` 2. `scripts/run_nightly.sh`
3. `POST /internal/jobs/nightly` 3. `POST /internal/jobs/nightly`
4. `PlaylistJobService.generate_for_all_connected_users()` 4. `PlaylistJobService.generate_for_all_connected_users()`
## Concurrency / Consistency ## Concurrency / consistency
- Генерация защищена одним `asyncio.Lock` (`generate_lock`) в `PlaylistJobService` - Generation is protected by a single `asyncio.Lock` (`generate_lock`) in `PlaylistJobService`
- предотвращает одновременные run'ы и гонки обновления history - prevents overlapping runs and history update races
- Большинство операций run выполняются в одной DB session - Most run operations happen in one DB session
- Ошибки внутри run переводят запись run в `failed` - Errors inside a run mark the run as `failed`
## Алгоритм рекомендаций (кратко) ## Recommendation algorithm (summary)
Подробно см. `README.md`, но архитектурно pipeline такой: Detailed explanation is in `README.md`, but architecturally the pipeline is:
1. Seed profile (recent + liked) 1. Build seed profile (recent + liked)
2. Candidate pool (Spotify + Last.fm + fallback search) 2. Collect candidate pool (Spotify + Last.fm + fallback search)
3. Dedupe 3. Deduplicate
4. Rank (score penalties/boosts) 4. Rank (penalties/boosts)
5. Select (min_new_ratio + artist caps) 5. Select (min_new_ratio + artist caps)
6. Persist stats/history 6. Persist stats/history
## Конфигурация ## Configuration
Основные env-переменные (`app/config.py`): Main environment variables (`app/config.py`):
- `TELEGRAM_BOT_TOKEN` - `TELEGRAM_BOT_TOKEN`
- `SPOTIFY_CLIENT_ID` - `SPOTIFY_CLIENT_ID`
@@ -341,23 +343,23 @@ Run log генерации:
- `RECENT_DAYS_WINDOW` - `RECENT_DAYS_WINDOW`
- `PLAYLIST_VISIBILITY` - `PLAYLIST_VISIBILITY`
## Диагностика / Наблюдаемость ## Diagnostics / observability
Сейчас: Current state:
- основной feedback идет через Telegram сообщения и `playlist_runs.notes` - primary feedback comes from Telegram messages and `playlist_runs.notes`
- HTTP `/health` для liveness - HTTP `/health` for liveness
- тесты покрывают критичные Spotify routes и части recommendation pipeline - tests cover critical Spotify routes and parts of the recommendation pipeline
Что можно улучшить: Possible improvements:
- структурированные логи по source coverage (сколько кандидатов из каждого источника) - structured logs for source coverage (how many candidates from each source)
- метрики latency/ошибок Spotify/Last.fm - metrics for Spotify/Last.fm errors and latency
- отдельный debug endpoint для dry-run (без создания playlist) - dedicated debug dry-run endpoint (without creating a playlist)
## Известные ограничения ## Known limitations
- SQLite подходит для small-scale / single-node сценария - SQLite is suitable for small-scale / single-node setups
- Telegram polling + FastAPI живут в одном процессе/контейнере - Telegram polling + FastAPI run in the same process/container
- per-user timezone используется ограниченно (cron общий) - per-user timezone support is limited (cron is global)
- внешние API ограничения (Spotify/Last.fm) могут различаться между приложениями/аккаунтами - external API limitations (Spotify/Last.fm) vary by app/account

345
README.md
View File

@@ -1,64 +1,64 @@
# Spotify Daily Vibe Bot (Telegram + Spotify + Docker) # Spotify Daily Vibe Bot (Telegram + Spotify + Docker)
Готовый backend-сервис, который: Ready-to-run backend service that:
- привязывается к вашему Spotify-аккаунту - connects to your Spotify account
- читает лайкнутые треки (`Liked Songs`) - reads your liked tracks (`Liked Songs`)
- учитывает недавние прослушивания (последние дни) - uses your recent listening history
- генерирует Spotify-плейлист с похожим вайбом по команде `/generate` - generates a Spotify playlist with a similar vibe via `/generate`
- опционально может запускаться по расписанию через `cron` - can optionally run on a schedule via `cron`
- минимизирует повторы и старается держать `>=80%` новых треков (не лайкнутых и не рекомендованных ранее ботом) - minimizes repeats and tries to keep `>=80%` of tracks "new" (not liked and not previously recommended by the bot)
- управляется через Telegram - is controlled via Telegram
- запускается в Docker (`app`, опционально `cron`) - runs in Docker (`app`, optional `cron`)
## Что внутри ## What's inside
- `FastAPI` backend (OAuth callback + internal job endpoint) - `FastAPI` backend (OAuth callback + internal job endpoint)
- `python-telegram-bot` (polling) - `python-telegram-bot` (polling)
- `SQLite` (история рекомендаций, кэш лайкнутых, run log) - `SQLite` (recommendation history, liked-track cache, run log)
- `supercronic` в отдельном контейнере для nightly cron trigger (опционально) - `supercronic` in a separate container for nightly cron trigger (optional)
## Важный момент по Spotify API ## Important note about the Spotify API
Spotify endpoint `/recommendations` может быть ограничен/недоступен для некоторых приложений. В сервисе реализован fallback: Spotify endpoint `/recommendations` may be limited/unavailable for some apps. The service includes fallbacks:
- Spotify recommendations (если доступен) - Spotify recommendations (if available)
- top tracks по артистам из вашего recent listening / liked library - top tracks by artists from your recent listening / liked library
- Spotify search по seed-артистам (если recommendations/top-tracks недоступны) - Spotify search by seed artists (fallback when recommendations/top-tracks are unavailable)
- optional Last.fm similarity (очень желательно для лучшего "вайба") - optional Last.fm similarity (very helpful for better "vibe" quality)
Для лучшего качества рекомендаций рекомендуется добавить `LASTFM_API_KEY`. For better recommendation quality, adding `LASTFM_API_KEY` is recommended.
## Быстрый старт ## Quick Start
1. Создайте Telegram-бота через `@BotFather` и получите токен. 1. Create a Telegram bot via `@BotFather` and get a token.
2. Создайте Spotify App: https://developer.spotify.com/dashboard 2. Create a Spotify App: https://developer.spotify.com/dashboard
3. В Spotify App добавьте Redirect URI (должен совпасть 1 в 1), например: 3. Add a Redirect URI in the Spotify App (must match exactly), for example:
- `https://your-domain.com/auth/spotify/callback` - `https://your-domain.com/auth/spotify/callback`
- или для локальной разработки через tunnel: `https://xxxx.ngrok-free.app/auth/spotify/callback` - or for local development via tunnel: `https://xxxx.ngrok-free.app/auth/spotify/callback`
4. Скопируйте `.env.example` в `.env` и заполните значения. 4. Copy `.env.example` to `.env` and fill in the values.
5. Запустите: 5. Start:
```bash ```bash
docker compose up -d --build docker compose up -d --build
``` ```
По умолчанию это поднимет только `app` (ручной режим через Telegram `/generate`). By default this starts only `app` (manual mode via Telegram `/generate`).
Если захотите включить ночной `cron`, запустите отдельно: If you want nightly `cron`, start it separately:
```bash ```bash
docker compose --profile cron up -d cron docker compose --profile cron up -d cron
``` ```
6. Откройте Telegram и напишите боту: 6. Open Telegram and message the bot:
- `/start` - `/start`
- `/connect` (получите ссылку на Spotify auth) - `/connect` (get the Spotify auth link)
- после подключения: `/generate` - after connecting: `/generate`
## Настройка `.env` ## `.env` configuration
Минимально обязательные поля: Minimum required fields:
- `TELEGRAM_BOT_TOKEN` - `TELEGRAM_BOT_TOKEN`
- `SPOTIFY_CLIENT_ID` - `SPOTIFY_CLIENT_ID`
@@ -66,237 +66,238 @@ docker compose --profile cron up -d cron
- `SPOTIFY_REDIRECT_URI` - `SPOTIFY_REDIRECT_URI`
- `INTERNAL_JOB_TOKEN` - `INTERNAL_JOB_TOKEN`
Рекомендуемые: Recommended:
- `LASTFM_API_KEY` (улучшает похожесть треков) - `LASTFM_API_KEY` (improves similarity quality)
- `APP_TIMEZONE` / `TZ` - `APP_TIMEZONE` / `TZ`
- `SPOTIFY_DEFAULT_MARKET` (двухбуквенный код страны, например `NL`, `DE`, `US`) - `SPOTIFY_DEFAULT_MARKET` (two-letter country code, e.g. `NL`, `DE`, `US`)
- `CRON_SCHEDULE` (например `15 2 * * *`, только если включаете `cron`) - `CRON_SCHEDULE` (e.g. `15 2 * * *`, only if you enable `cron`)
## Telegram команды ## Telegram commands
- `/connect` — привязать Spotify - `/connect` - connect Spotify
- `/status` — статус подключения и последний плейлист - `/status` - connection status and latest playlist run
- `/generate` — сгенерировать плейлист сейчас - `/generate` - generate a playlist now
- `/latest` — ссылка на последний плейлист - `/latest` - latest playlist link
- `/setsize 30` — размер плейлиста (5..100) - `/setsize 30` - playlist size (5..100)
- `/setratio 0.8` — целевая доля новых треков (0.5..1.0) - `/setratio 0.8` - target new-track ratio (0.5..1.0)
- `/sync` — принудительно обновить лайкнутые треки - `/sync` - force sync liked tracks
- `/lang ru|en` - switch bot language
## Алгоритм подбора рекомендаций ## Recommendation Algorithm
Ниже описан фактический пайплайн генерации плейлиста (как он сейчас работает в коде). This is the actual playlist generation pipeline used by the current code.
### 1. Подготовка входных данных ### 1. Input preparation
Перед генерацией бот: Before generation, the bot:
- обновляет Spotify access token по refresh token (если нужно) - refreshes Spotify access token if needed
- синхронизирует лайкнутые треки из `Liked Songs` в локальный кэш (`saved_tracks`) - syncs liked tracks from `Liked Songs` into the local cache (`saved_tracks`)
- загружает recent listening за окно `RECENT_DAYS_WINDOW` (по умолчанию `5` дней) - loads recent listening for the `RECENT_DAYS_WINDOW` period (default `5` days)
- загружает историю ранее рекомендованных треков (`recommendation_history`) - loads history of previously recommended tracks (`recommendation_history`)
### 2. Построение seed-профиля ### 2. Seed profile construction
Бот собирает seed'ы из двух источников: recent plays и liked library. The bot builds seeds from two sources: recent plays and liked library.
- Recent plays: - Recent plays:
- каждый recent track получает вес с убыванием по позиции (более свежие прослушивания важнее) - each track gets a recency-weighted score (newer plays matter more)
- накапливаются веса по трекам и артистам - weights are accumulated for both tracks and artists
- Liked tracks: - Liked tracks:
- берется срез последних лайков (`~120`) - takes a slice of recent likes (`~120`)
- плюс случайная выборка из более старых лайков (для разнообразия) - adds a random sample from older likes (for exploration/diversity)
- из них также накапливаются веса по артистам - accumulates artist weights from this pool as well
На выходе seed-профиля формируются: Seed profile output includes:
- `seed_track_ids` (до ~10 треков) - `seed_track_ids` (up to ~10 tracks)
- `seed_artists` (до ~20 артистов) - `seed_artists` (up to ~20 artists)
- `seed_artist_names` (для Last.fm и Spotify Search fallback) - `seed_artist_names` (used by Last.fm and Spotify Search fallback)
- `recent_track_meta` (нужно для Last.fm track-similar) - `recent_track_meta` (used for Last.fm track-similar lookups)
### 3. Сбор кандидатов (candidate pool) ### 3. Candidate collection (candidate pool)
Бот собирает общий пул кандидатов из нескольких источников и дедуплицирует их. The bot builds a shared candidate pool from multiple sources and deduplicates results.
Источники (по порядку): Sources (in order):
1. `Spotify recommendations` 1. `Spotify recommendations`
- вызывается батчами - requested in batches
- соблюдается лимит Spotify: максимум `5` seed'ов на запрос (суммарно track + artist) - respects Spotify limit: max `5` seeds per request (track + artist combined)
2. `Spotify artist top tracks` 2. `Spotify artist top tracks`
- по seed-артистам - by seed artists
3. `Spotify search` по seed-артистам (fallback) 3. `Spotify search` by seed artists (fallback)
- используется, если recommendations / top-tracks ограничены или дали мало результатов - used when recommendations / top-tracks are restricted or return too few results
4. `Last.fm track similar` -> `Spotify search` 4. `Last.fm track similar` -> `Spotify search`
- для recent seed-треков - for recent seed tracks
5. `Last.fm artist similar` -> `Spotify search` 5. `Last.fm artist similar` -> `Spotify search`
- для seed-артистов - for seed artists
Если Spotify/Last.fm отдают ошибки на отдельных вызовах, бот старается деградировать мягко (использовать другие источники), а не валить весь run сразу. If Spotify/Last.fm fails on individual calls, the bot tries to degrade gracefully (use other sources) instead of failing the whole run immediately.
### 4. Дедупликация кандидатов ### 4. Candidate deduplication
Кандидаты дедуплицируются: Candidates are deduplicated:
- по `spotify_track_id` - by `spotify_track_id`
- по нормализованной сигнатуре `track_name + artist_names` (на случай дублей / разных версий) - by normalized signature `track_name + artist_names` (to catch duplicates / alternate versions)
Если один и тот же трек найден из нескольких источников: If the same track is found via multiple sources:
- сохраняется лучший score - the best score is kept
- источник объединяется (например, `source1+source2`) - the source field is merged (e.g. `source1+source2`)
### 5. Фильтрация и ранжирование ### 5. Filtering and ranking
Базовая логика: Base logic:
- сначала исключаются треки, которые уже есть в ваших лайках (`liked_ids`) - first, tracks already in your likes (`liked_ids`) are excluded
- если после этого пул пустой, включается fallback: - if that leaves an empty pool, a fallback is enabled:
- разрешается использовать already-liked треки (с penalty), чтобы не падать с пустым результатом - already-liked tracks may be used (with a penalty) so the run does not fail with an empty result
Дополнительные коррекции score: Additional score adjustments:
- penalty за уже рекомендованные раньше ботом (`history_ids`) - penalty for tracks previously recommended by the bot (`history_ids`)
- penalty за лайкнутые (если включился liked fallback) - penalty for liked tracks (only if liked fallback is active)
- небольшой boost за коллаборации / нескольких артистов - small boost for collaborations / multiple artists
- небольшой boost за накопленные причины/источники - small boost for tracks with multiple source/reason signals
- popularity scoring слегка тяготеет к mid-popularity (не только мейнстрим и не только deep cuts) - popularity scoring slightly favors mid-popularity tracks (not only mainstream and not only obscure tracks)
### 6. Отбор финального списка (selection) ### 6. Final selection
После ранжирования кандидаты делятся на: After ranking, candidates are split into:
- `novel` — не были рекомендованы ранее и не в лайках - `novel` - not previously recommended and not in likes
- `reused` — уже были рекомендованы или (fallback) уже лайкнуты - `reused` - previously recommended or (fallback case) already liked
Далее бот: Then the bot:
- сначала пытается набрать минимум по `min_new_ratio` - first tries to satisfy `min_new_ratio`
- соблюдает artist caps (ограничение количества треков одного артиста) - enforces artist caps (limit tracks per artist)
- если новых треков недостаточно, ослабляет ограничения - relaxes caps if there are not enough new tracks
- затем дозаполняет повторными кандидатами - fills the remainder with reused candidates
Результат: Result includes:
- `tracks` — финальный порядок треков - `tracks` - final ordered playlist tracks
- `new_count` / `reused_count` - `new_count` / `reused_count`
- `notes` — пояснение, если не удалось выдержать target по новым трекам - `notes` - explanation if the target new ratio could not be met
### 7. Создание плейлиста и запись истории ### 7. Playlist creation and history persistence
После сборки списка бот: After the final track list is selected, the bot:
- создает Spotify playlist - creates a Spotify playlist
- добавляет треки - adds tracks to it
- записывает run в `playlist_runs` и `playlist_run_tracks` - writes the run to `playlist_runs` and `playlist_run_tracks`
- обновляет `recommendation_history` - updates `recommendation_history`
- сохраняет `latest_playlist_url` у пользователя - stores `latest_playlist_url` for the user
## Как работает анти-повтор ## Anti-repeat behavior
Бот хранит: The bot stores:
- все треки, которые уже рекомендовал раньше - all tracks it has recommended before
- все ваши лайкнутые треки (кэш обновляется) - all your liked tracks (cached and refreshed)
При сборке нового плейлиста: When building a new playlist:
- сначала исключает лайкнутые треки (если это возможно) - it first excludes liked tracks (when possible)
- отдает приоритет трекам, которых не было в рекомендациях ранее - prioritizes tracks that have not been recommended before
- если новых треков не хватает, дозаполняет повторами из истории - fills with history repeats only if there are not enough new tracks
- если кандидаты есть только среди лайкнутых, может использовать liked fallback вместо полного фейла run - may use a liked-track fallback instead of failing the run if all candidates are already liked
- пишет статистику в БД (`new / reused`) - stores `new / reused` stats in the DB
Если в доступном пуле не хватает новых треков для `80%`, бот сообщит об этом в статусе run. If there are not enough new tracks to satisfy the `80%` target, the run status includes a note explaining that.
## Cron (ночной запуск) ## Cron (nightly run)
По умолчанию `cron` отключен (manual-first режим: запускаете `/generate` вручную в Telegram). `cron` is disabled by default (manual-first mode: run `/generate` manually in Telegram).
В `docker-compose.yml` сервис `cron` помечен профилем `cron`, поэтому он не стартует при обычном: In `docker-compose.yml`, the `cron` service is under profile `cron`, so it does not start with a normal:
```bash ```bash
docker compose up -d --build docker compose up -d --build
``` ```
Если хотите включить ночной запуск, поднимите его отдельно: To enable nightly runs:
```bash ```bash
docker compose --profile cron up -d cron docker compose --profile cron up -d cron
``` ```
`cron` по `CRON_SCHEDULE` вызывает внутренний endpoint: `cron` calls the internal endpoint on schedule:
- `POST /internal/jobs/nightly` - `POST /internal/jobs/nightly`
Измените время через `.env`: Change time via `.env`:
```env ```env
CRON_SCHEDULE=15 2 * * * CRON_SCHEDULE=15 2 * * *
TZ=Europe/Moscow TZ=Europe/Amsterdam
``` ```
Отключить обратно: Disable again:
```bash ```bash
docker compose stop cron docker compose stop cron
``` ```
## Хранилище данных ## Data storage
- SQLite БД: `./data/app.db` - SQLite DB: `./data/app.db`
Эта папка примонтирована как volume, поэтому данные переживают перезапуск контейнеров. This folder is mounted as a Docker volume, so data persists across container restarts.
## Проверка работы ## Health check / verification
- `GET /health` должен вернуть `{"ok": true}` - `GET /health` should return `{"ok": true}`
- после `/generate` в Telegram появится ссылка на Spotify playlist - after `/generate`, Telegram should send a Spotify playlist link
## Типичный деплой ## Typical deployment
- VPS + Docker Compose - VPS + Docker Compose
- `APP_BASE_URL` = публичный URL сервиса - `APP_BASE_URL` = public service URL
- `SPOTIFY_REDIRECT_URI` = `${APP_BASE_URL}/auth/spotify/callback` - `SPOTIFY_REDIRECT_URI` = `${APP_BASE_URL}/auth/spotify/callback`
- Telegram работает через polling (webhook не нужен) - Telegram runs via polling (no webhook required)
- `cron` можно не включать совсем, если генерация только вручную - `cron` can remain disabled if you only want manual generation
## Архитектура ## Architecture
Подробное описание архитектуры приложения, потоков данных и таблиц БД вынесено в `DESIGN.md`. Detailed architecture, data flow, and DB table docs are in `DESIGN.md`.
## Feature Plans ## Feature Plans
Ниже план ближайших улучшений (roadmap), которые хорошо ложатся на текущую архитектуру. Roadmap items that fit the current architecture well:
- Явный feedback loop: - Explicit feedback loop:
- команды вроде `/ban`, `/unban`, `/prefer` - commands like `/ban`, `/unban`, `/prefer`
- отдельная blacklist-таблица, чтобы "не понравилось" != "просто не лайкнул" - separate blacklist table so "didn't like it" != "just didn't save it"
- Настройки anti-repeat: - Anti-repeat controls:
- жесткий запрет повторов на N дней/недель - hard no-repeat window (N days/weeks)
- отдельные правила для liked / previously recommended - separate rules for liked / previously recommended tracks
- Explainability / debug: - Explainability / debug:
- why-this-track (показать источник, score, причины попадания) - why-this-track (source, score, reasons)
- dry-run endpoint/команда без создания плейлиста - dry-run endpoint/command without creating a playlist
- Тонкая настройка алгоритма: - Fine-tuning the algorithm:
- веса источников (Spotify / Last.fm / search fallback) - source weights (Spotify / Last.fm / search fallback)
- режимы генерации (explore / familiar / mixed) - generation modes (explore / familiar / mixed)
- Улучшение источников кандидатов: - Better candidate sources:
- дополнительные музыкальные источники / метаданные - additional music metadata sources
- более умная работа с жанрами/артист-кластерами - smarter genre/artist clustering
- Персональный scheduler: - Personal scheduler:
- per-user timezone и per-user cron schedule - per-user timezone and per-user cron schedule
- выбор дней недели / времени генерации - weekday / time selection
- Наблюдаемость: - Observability:
- структурированные логи по source coverage и причинам фильтрации - structured logs for source coverage and filtering reasons
- простые метрики по ошибкам Spotify/Last.fm и latency - basic metrics for Spotify/Last.fm errors and latency
- Хранилище / масштабирование: - Storage / scaling:
- миграции (Alembic) - migrations (Alembic)
- Postgres вместо SQLite для multi-user сценариев - Postgres instead of SQLite for multi-user usage
## Ограничения / улучшения (если захотите дальше) ## Limitations / future improvements
- Персонификация по timezone на пользователя (сейчас cron общий, но user-specific generation поддерживается вручную) - Per-user timezone support is only partially used today (cron is global, though manual per-user generation is supported)
- Больше источников похожих треков (например, MusicBrainz/Discogs mapping) - More candidate sources could improve quality (e.g. MusicBrainz/Discogs mapping)
- Выделенный Postgres вместо SQLite для multi-user нагрузки - Postgres would be better than SQLite for higher multi-user load