From bd23a9da8a48cf930819b8f28d1e9503dd317545 Mon Sep 17 00:00:00 2001 From: heboba Date: Thu, 26 Feb 2026 20:25:20 +0000 Subject: [PATCH] Translate documentation to english --- DESIGN.md | 334 ++++++++++++++++++++++++++-------------------------- README.md | 345 +++++++++++++++++++++++++++--------------------------- 2 files changed, 341 insertions(+), 338 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 227b0f3..8de3bfa 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,86 +1,86 @@ # Design / Architecture -## Цель +## Goal -Сервис генерирует Spotify playlist "daily vibe" на основе: +The service generates a Spotify "daily vibe" playlist based on: -- recent listening пользователя -- кэша liked tracks -- истории уже выданных рекомендаций +- the user's recent listening +- 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` приложение - - healthcheck +- `FastAPI` application + - health check - Spotify OAuth start/callback - - internal endpoint для cron (`/internal/jobs/nightly`) + - internal endpoint for cron (`/internal/jobs/nightly`) - `TelegramBotRunner` (polling) - - принимает команды пользователей - - запускает генерацию и отправляет статусы + - handles user commands + - starts generation and sends status updates - `PlaylistJobService` - - orchestration одного run (token -> sync likes -> candidates -> playlist -> persist) + - orchestrates a single run (token -> sync likes -> candidates -> playlist -> persist) - `RecommendationEngine` - - строит seed profile - - собирает candidate pool - - ранжирует и отбирает треки + - builds seed profile + - collects candidate pool + - ranks and selects tracks - `SpotifyClient` / `LastFmClient` - - внешние API вызовы -- `SQLite` (через SQLAlchemy async) - - пользователи, кэш лайков, история рекомендаций, run log + - external API calls +- `SQLite` (via async SQLAlchemy) + - 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`) -2. Создается async SQLAlchemy engine и session factory (`app/db/session.py`) -3. Выполняется `create_all` (автосоздание таблиц) -4. Создается общий `httpx.AsyncClient` -5. Создаются клиенты: +1. Load `Settings` (`app/config.py`) +2. Create async SQLAlchemy engine and session factory (`app/db/session.py`) +3. Run `create_all` (auto-create tables) +4. Create shared `httpx.AsyncClient` +5. Create API clients: - `SpotifyClient` - `LastFmClient` -6. Создаются сервисы: +6. Create services: - `SpotifyAuthService` - `RecommendationEngine` - `PlaylistJobService` -7. Инициализируется `TelegramBotRunner` и запускается polling -8. Все объекты складываются в `app.state.runtime` и `app.state.services` +7. Initialize `TelegramBotRunner` and start polling +8. Store runtime/service objects in `app.state.runtime` and `app.state.services` -На shutdown: +On shutdown: -- останавливается Telegram polling -- закрывается `httpx.AsyncClient` -- закрывается DB engine +- stop Telegram polling +- close `httpx.AsyncClient` +- dispose DB engine -## Контейнеры / Deployment +## Containers / deployment -`docker-compose.yml`: +`docker-compose.yml` defines: -- `app` (основной сервис, FastAPI + Telegram polling) -- `cron` (опциональный сервис с `supercronic`) +- `app` (main service, FastAPI + Telegram polling) +- `cron` (optional service with `supercronic`) -Важно: +Important: -- `cron` помечен `profiles: ["cron"]` и по умолчанию не стартует -- manual-first режим: пользователь генерирует плейлисты через Telegram `/generate` +- `cron` is under `profiles: ["cron"]` and does not start by default +- 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 ` +- `POST /internal/jobs/nightly` with `Authorization: Bearer ` -## Слои приложения +## 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: @@ -89,18 +89,18 @@ Endpoints: - `GET /auth/spotify/callback` - `POST /internal/jobs/nightly` -Особенности: +Notes: -- OAuth callback после успеха отправляет сообщение в Telegram пользователю -- internal nightly endpoint защищен `INTERNAL_JOB_TOKEN` +- OAuth callback sends a Telegram notification to the user on success +- 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` - `/help` @@ -111,118 +111,120 @@ Endpoints: - `/setsize` - `/setratio` - `/sync` +- `/lang` -Особенности: +Notes: -- `/generate` запускает `PlaylistJobService.generate_for_user(..., force=True, notify=False)` -- `/sync` только обновляет кэш лайков -- у каждой команды свой короткий DB session через `session_factory` +- `/generate` calls `PlaylistJobService.generate_for_user(..., force=True, notify=False)` +- `/sync` only refreshes liked tracks cache +- 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`) -Роли: +Responsibilities: -- создание OAuth state -- обмен `code` на токены +- create OAuth state +- exchange `code` for tokens - refresh access token -- защита от истекшего access token +- ensure valid access token before Spotify calls -Особенности: +Notes: -- сравнение дат нормализуется в UTC (важно для SQLite naive datetime) -- сохраняет scopes и expiry в таблице `users` +- datetime comparison is normalized to UTC (important for SQLite naive datetimes) +- stores scopes and expiry on the `users` row #### `RecommendationEngine` (`app/services/recommendation.py`) -Роли: +Responsibilities: -- sync liked tracks в локальный кэш +- sync liked tracks into local cache - build seed profile -- collect candidates из нескольких источников -- rank/select итоговый список +- collect candidates from multiple sources +- rank/select final track list -Текущие источники кандидатов: +Current candidate sources: - Spotify recommendations - Spotify artist top tracks -- Spotify search (seed artist fallback) +- Spotify search (seed-artist fallback) - Last.fm track similar -> Spotify search - Last.fm artist similar -> Spotify search -Ключевые особенности: +Key implementation details: -- соблюдение лимита Spotify recommendations: максимум `5` seed'ов на запрос -- мягкая деградация при частичных ошибках источников -- liked fallback (если весь пул оказался уже в лайках) +- respects Spotify recommendations seed limit: max `5` seeds per request +- degrades gracefully when some sources fail +- includes liked fallback (if all candidates are already liked) #### `PlaylistJobService` (`app/services/playlist_job.py`) -Роли: +Responsibilities: -- orchestration полного run -- создание Spotify playlist и добавление треков -- запись run и треков в БД -- обновление recommendation history -- отправка уведомления в Telegram (если задан notifier) +- orchestrate an end-to-end playlist generation run +- create Spotify playlist and add tracks +- persist run details and track list +- update recommendation history +- send Telegram notifications (if notifier is configured) -Порядок выполнения run: +Run sequence: -1. Проверка пользователя / Spotify connection -2. Создание записи `playlist_runs` со статусом `running` -3. Получение valid access token +1. Validate user / Spotify connection +2. Create `playlist_runs` row with `running` status +3. Get valid access token 4. Sync liked tracks -5. Сборка плейлиста через `RecommendationEngine` -6. Создание playlist в Spotify -7. Добавление треков в playlist -8. Сохранение run-трека/истории/метаданных -9. Commit и возврат `JobOutcome` +5. Build playlist via `RecommendationEngine` +6. Create playlist in Spotify +7. Add tracks to playlist +8. Persist run tracks / history / metadata +9. Commit and return `JobOutcome` -При ошибке: +On error: - `playlist_runs.status = failed` -- в `notes` записывается сообщение ошибки +- error message is written to `notes` -## Client Layer +## Client layer ### `SpotifyClient` (`app/clients/spotify.py`) -Инкапсулирует Spotify Web API. +Encapsulates Spotify Web API calls. -Что важно в текущей реализации: +Important implementation choices: -- `create_playlist()` использует `POST /me/playlists` - - это выбранный маршрут, потому что `POST /users/{id}/playlists` может давать `403` в некоторых аккаунтах/приложениях -- `add_playlist_items()` использует `POST /playlists/{playlist_id}/items` - - маршрут `/tracks` в практике может отдавать `403`, хотя `/items` работает -- `delete_playlist()` вызывает `DELETE /playlists/{playlist_id}/followers` - - это "unfollow" (Spotify не поддерживает hard delete playlist) -- встроены retry на `429` (rate-limit) с `Retry-After` +- `create_playlist()` uses `POST /me/playlists` + - chosen because `POST /users/{id}/playlists` can return `403` in some app/account combinations +- `add_playlist_items()` uses `POST /playlists/{playlist_id}/items` + - `/tracks` may return `403` while `/items` succeeds +- `delete_playlist()` uses `DELETE /playlists/{playlist_id}/followers` + - this is "unfollow" (Spotify does not support hard-delete of playlists) +- built-in retry for `429` rate limiting using `Retry-After` ### `LastFmClient` (`app/clients/lastfm.py`) -Используется как optional enrichment layer. +Optional enrichment source for similarity. -- может быть отключен (если `LASTFM_API_KEY` пустой) -- ошибки Last.fm не должны ломать весь run, если другие источники работают +- can be disabled (empty `LASTFM_API_KEY`) +- 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` -Хранит: +Stores: - Telegram identity (`telegram_chat_id`, `telegram_username`) - Spotify identity/tokens/scopes (`spotify_user_id`, access/refresh token, expiry, scopes) -- пользовательские настройки (`playlist_size`, `min_new_ratio`, timezone) -- последние результаты (`last_generated_date`, `latest_playlist_id`, `latest_playlist_url`) +- user settings (`playlist_size`, `min_new_ratio`, timezone) +- last outputs (`last_generated_date`, `latest_playlist_id`, `latest_playlist_url`) #### `auth_states` -Временные OAuth state для callback: +Temporary OAuth state for callback: - `state` - `telegram_chat_id` @@ -230,15 +232,15 @@ Endpoints: #### `saved_tracks` -Локальный кэш `Liked Songs` пользователя: +Local cache of the user's `Liked Songs`: - `spotify_track_id` -- название/артисты/album/popularity +- track/artist metadata, album, popularity - `added_at` #### `recommendation_history` -История ранее рекомендованных треков: +History of previously recommended tracks: - `spotify_track_id` - `first_recommended_at` @@ -247,30 +249,30 @@ Endpoints: #### `playlist_runs` -Run log генерации: +Playlist generation run log: -- статус (`running/success/failed`) -- метаданные Spotify playlist -- статистика (`total/new/reused`) +- status (`running/success/failed`) +- Spotify playlist metadata +- stats (`total/new/reused`) - `notes` #### `playlist_run_tracks` -Снимок состава конкретного run: +Snapshot of tracks in a specific run: - track id / name / artists -- source (из какого источника пришел) -- позиция +- source (which source produced the track) +- position - `is_new_to_bot` -### Repository Layer (`app/db/repositories.py`) +### Repository layer (`app/db/repositories.py`) -Паттерн: +Pattern: -- thin repositories над SQLAlchemy AsyncSession -- изолируют CRUD/query-логику от service layer +- thin repositories over `AsyncSession` +- isolates CRUD/query logic from the service layer -Примеры: +Repositories include: - `UserRepository` - `AuthStateRepository` @@ -278,55 +280,55 @@ Run log генерации: - `RecommendationHistoryRepository` - `PlaylistRunRepository` -## Потоки данных +## Data flows -### OAuth Flow +### OAuth flow 1. Telegram `/connect` 2. `SpotifyAuthService.create_connect_url()` -3. Пользователь идет в Spotify auth page +3. User opens Spotify auth page 4. `GET /auth/spotify/callback` 5. `SpotifyAuthService.handle_callback()` -6. Токены и Spotify profile сохраняются в `users` -7. Пользователю отправляется сообщение в Telegram +6. Tokens and Spotify profile are saved to `users` +7. User receives a Telegram confirmation message -### Manual Generate Flow (`/generate`) +### Manual generation flow (`/generate`) 1. Telegram `/generate` 2. `PlaylistJobService.generate_for_user(..., force=True)` -3. Sync likes + recent listening + candidate collection -4. Playlist create + add items в Spotify +3. Sync likes + load recent listening + collect candidates +4. Create playlist + add items in Spotify 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` 3. `POST /internal/jobs/nightly` 4. `PlaylistJobService.generate_for_all_connected_users()` -## Concurrency / Consistency +## Concurrency / consistency -- Генерация защищена одним `asyncio.Lock` (`generate_lock`) в `PlaylistJobService` - - предотвращает одновременные run'ы и гонки обновления history -- Большинство операций run выполняются в одной DB session -- Ошибки внутри run переводят запись run в `failed` +- Generation is protected by a single `asyncio.Lock` (`generate_lock`) in `PlaylistJobService` + - prevents overlapping runs and history update races +- Most run operations happen in one DB session +- 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) -2. Candidate pool (Spotify + Last.fm + fallback search) -3. Dedupe -4. Rank (score penalties/boosts) +1. Build seed profile (recent + liked) +2. Collect candidate pool (Spotify + Last.fm + fallback search) +3. Deduplicate +4. Rank (penalties/boosts) 5. Select (min_new_ratio + artist caps) 6. Persist stats/history -## Конфигурация +## Configuration -Основные env-переменные (`app/config.py`): +Main environment variables (`app/config.py`): - `TELEGRAM_BOT_TOKEN` - `SPOTIFY_CLIENT_ID` @@ -341,23 +343,23 @@ Run log генерации: - `RECENT_DAYS_WINDOW` - `PLAYLIST_VISIBILITY` -## Диагностика / Наблюдаемость +## Diagnostics / observability -Сейчас: +Current state: -- основной feedback идет через Telegram сообщения и `playlist_runs.notes` -- HTTP `/health` для liveness -- тесты покрывают критичные Spotify routes и части recommendation pipeline +- primary feedback comes from Telegram messages and `playlist_runs.notes` +- HTTP `/health` for liveness +- tests cover critical Spotify routes and parts of the recommendation pipeline -Что можно улучшить: +Possible improvements: -- структурированные логи по source coverage (сколько кандидатов из каждого источника) -- метрики latency/ошибок Spotify/Last.fm -- отдельный debug endpoint для dry-run (без создания playlist) +- structured logs for source coverage (how many candidates from each source) +- metrics for Spotify/Last.fm errors and latency +- dedicated debug dry-run endpoint (without creating a playlist) -## Известные ограничения +## Known limitations -- SQLite подходит для small-scale / single-node сценария -- Telegram polling + FastAPI живут в одном процессе/контейнере -- per-user timezone используется ограниченно (cron общий) -- внешние API ограничения (Spotify/Last.fm) могут различаться между приложениями/аккаунтами +- SQLite is suitable for small-scale / single-node setups +- Telegram polling + FastAPI run in the same process/container +- per-user timezone support is limited (cron is global) +- external API limitations (Spotify/Last.fm) vary by app/account diff --git a/README.md b/README.md index 2ba957d..86a4768 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,64 @@ # Spotify Daily Vibe Bot (Telegram + Spotify + Docker) -Готовый backend-сервис, который: +Ready-to-run backend service that: -- привязывается к вашему Spotify-аккаунту -- читает лайкнутые треки (`Liked Songs`) -- учитывает недавние прослушивания (последние дни) -- генерирует Spotify-плейлист с похожим вайбом по команде `/generate` -- опционально может запускаться по расписанию через `cron` -- минимизирует повторы и старается держать `>=80%` новых треков (не лайкнутых и не рекомендованных ранее ботом) -- управляется через Telegram -- запускается в Docker (`app`, опционально `cron`) +- connects to your Spotify account +- reads your liked tracks (`Liked Songs`) +- uses your recent listening history +- generates a Spotify playlist with a similar vibe via `/generate` +- can optionally run on a schedule via `cron` +- minimizes repeats and tries to keep `>=80%` of tracks "new" (not liked and not previously recommended by the bot) +- is controlled via Telegram +- runs in Docker (`app`, optional `cron`) -## Что внутри +## What's inside - `FastAPI` backend (OAuth callback + internal job endpoint) - `python-telegram-bot` (polling) -- `SQLite` (история рекомендаций, кэш лайкнутых, run log) -- `supercronic` в отдельном контейнере для nightly cron trigger (опционально) +- `SQLite` (recommendation history, liked-track cache, run log) +- `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 (если доступен) -- top tracks по артистам из вашего recent listening / liked library -- Spotify search по seed-артистам (если recommendations/top-tracks недоступны) -- optional Last.fm similarity (очень желательно для лучшего "вайба") +- Spotify recommendations (if available) +- top tracks by artists from your recent listening / liked library +- Spotify search by seed artists (fallback when recommendations/top-tracks are unavailable) +- 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` и получите токен. -2. Создайте Spotify App: https://developer.spotify.com/dashboard -3. В Spotify App добавьте Redirect URI (должен совпасть 1 в 1), например: +1. Create a Telegram bot via `@BotFather` and get a token. +2. Create a Spotify App: https://developer.spotify.com/dashboard +3. Add a Redirect URI in the Spotify App (must match exactly), for example: - `https://your-domain.com/auth/spotify/callback` - - или для локальной разработки через tunnel: `https://xxxx.ngrok-free.app/auth/spotify/callback` -4. Скопируйте `.env.example` в `.env` и заполните значения. -5. Запустите: + - or for local development via tunnel: `https://xxxx.ngrok-free.app/auth/spotify/callback` +4. Copy `.env.example` to `.env` and fill in the values. +5. Start: ```bash 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 docker compose --profile cron up -d cron ``` -6. Откройте Telegram и напишите боту: +6. Open Telegram and message the bot: - `/start` - - `/connect` (получите ссылку на Spotify auth) - - после подключения: `/generate` + - `/connect` (get the Spotify auth link) + - after connecting: `/generate` -## Настройка `.env` +## `.env` configuration -Минимально обязательные поля: +Minimum required fields: - `TELEGRAM_BOT_TOKEN` - `SPOTIFY_CLIENT_ID` @@ -66,237 +66,238 @@ docker compose --profile cron up -d cron - `SPOTIFY_REDIRECT_URI` - `INTERNAL_JOB_TOKEN` -Рекомендуемые: +Recommended: -- `LASTFM_API_KEY` (улучшает похожесть треков) +- `LASTFM_API_KEY` (improves similarity quality) - `APP_TIMEZONE` / `TZ` -- `SPOTIFY_DEFAULT_MARKET` (двухбуквенный код страны, например `NL`, `DE`, `US`) -- `CRON_SCHEDULE` (например `15 2 * * *`, только если включаете `cron`) +- `SPOTIFY_DEFAULT_MARKET` (two-letter country code, e.g. `NL`, `DE`, `US`) +- `CRON_SCHEDULE` (e.g. `15 2 * * *`, only if you enable `cron`) -## Telegram команды +## Telegram commands -- `/connect` — привязать Spotify -- `/status` — статус подключения и последний плейлист -- `/generate` — сгенерировать плейлист сейчас -- `/latest` — ссылка на последний плейлист -- `/setsize 30` — размер плейлиста (5..100) -- `/setratio 0.8` — целевая доля новых треков (0.5..1.0) -- `/sync` — принудительно обновить лайкнутые треки +- `/connect` - connect Spotify +- `/status` - connection status and latest playlist run +- `/generate` - generate a playlist now +- `/latest` - latest playlist link +- `/setsize 30` - playlist size (5..100) +- `/setratio 0.8` - target new-track ratio (0.5..1.0) +- `/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 (если нужно) -- синхронизирует лайкнутые треки из `Liked Songs` в локальный кэш (`saved_tracks`) -- загружает recent listening за окно `RECENT_DAYS_WINDOW` (по умолчанию `5` дней) -- загружает историю ранее рекомендованных треков (`recommendation_history`) +- refreshes Spotify access token if needed +- syncs liked tracks from `Liked Songs` into the local cache (`saved_tracks`) +- loads recent listening for the `RECENT_DAYS_WINDOW` period (default `5` days) +- 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 track получает вес с убыванием по позиции (более свежие прослушивания важнее) - - накапливаются веса по трекам и артистам + - each track gets a recency-weighted score (newer plays matter more) + - weights are accumulated for both tracks and artists - 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_artists` (до ~20 артистов) -- `seed_artist_names` (для Last.fm и Spotify Search fallback) -- `recent_track_meta` (нужно для Last.fm track-similar) +- `seed_track_ids` (up to ~10 tracks) +- `seed_artists` (up to ~20 artists) +- `seed_artist_names` (used by Last.fm and Spotify Search fallback) +- `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` - - вызывается батчами - - соблюдается лимит Spotify: максимум `5` seed'ов на запрос (суммарно track + artist) + - requested in batches + - respects Spotify limit: max `5` seeds per request (track + artist combined) 2. `Spotify artist top tracks` - - по seed-артистам -3. `Spotify search` по seed-артистам (fallback) - - используется, если recommendations / top-tracks ограничены или дали мало результатов + - by seed artists +3. `Spotify search` by seed artists (fallback) + - used when recommendations / top-tracks are restricted or return too few results 4. `Last.fm track similar` -> `Spotify search` - - для recent seed-треков + - for recent seed tracks 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` -- по нормализованной сигнатуре `track_name + artist_names` (на случай дублей / разных версий) +- by `spotify_track_id` +- by normalized signature `track_name + artist_names` (to catch duplicates / alternate versions) -Если один и тот же трек найден из нескольких источников: +If the same track is found via multiple sources: -- сохраняется лучший score -- источник объединяется (например, `source1+source2`) +- the best score is kept +- the source field is merged (e.g. `source1+source2`) -### 5. Фильтрация и ранжирование +### 5. Filtering and ranking -Базовая логика: +Base logic: -- сначала исключаются треки, которые уже есть в ваших лайках (`liked_ids`) -- если после этого пул пустой, включается fallback: - - разрешается использовать already-liked треки (с penalty), чтобы не падать с пустым результатом +- first, tracks already in your likes (`liked_ids`) are excluded +- if that leaves an empty pool, a fallback is enabled: + - 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 за лайкнутые (если включился liked fallback) -- небольшой boost за коллаборации / нескольких артистов -- небольшой boost за накопленные причины/источники -- popularity scoring слегка тяготеет к mid-popularity (не только мейнстрим и не только deep cuts) +- penalty for tracks previously recommended by the bot (`history_ids`) +- penalty for liked tracks (only if liked fallback is active) +- small boost for collaborations / multiple artists +- small boost for tracks with multiple source/reason signals +- 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` — не были рекомендованы ранее и не в лайках -- `reused` — уже были рекомендованы или (fallback) уже лайкнуты +- `novel` - not previously recommended and not in likes +- `reused` - previously recommended or (fallback case) already liked -Далее бот: +Then the bot: -- сначала пытается набрать минимум по `min_new_ratio` -- соблюдает artist caps (ограничение количества треков одного артиста) -- если новых треков недостаточно, ослабляет ограничения -- затем дозаполняет повторными кандидатами +- first tries to satisfy `min_new_ratio` +- 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` -- `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 -- добавляет треки -- записывает run в `playlist_runs` и `playlist_run_tracks` -- обновляет `recommendation_history` -- сохраняет `latest_playlist_url` у пользователя +- creates a Spotify playlist +- adds tracks to it +- writes the run to `playlist_runs` and `playlist_run_tracks` +- updates `recommendation_history` +- 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: -- сначала исключает лайкнутые треки (если это возможно) -- отдает приоритет трекам, которых не было в рекомендациях ранее -- если новых треков не хватает, дозаполняет повторами из истории -- если кандидаты есть только среди лайкнутых, может использовать liked fallback вместо полного фейла run -- пишет статистику в БД (`new / reused`) +- 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 +- may use a liked-track fallback instead of failing the run if all candidates are already liked +- 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 docker compose up -d --build ``` -Если хотите включить ночной запуск, поднимите его отдельно: +To enable nightly runs: ```bash docker compose --profile cron up -d cron ``` -`cron` по `CRON_SCHEDULE` вызывает внутренний endpoint: +`cron` calls the internal endpoint on schedule: - `POST /internal/jobs/nightly` -Измените время через `.env`: +Change time via `.env`: ```env CRON_SCHEDULE=15 2 * * * -TZ=Europe/Moscow +TZ=Europe/Amsterdam ``` -Отключить обратно: +Disable again: ```bash 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}` -- после `/generate` в Telegram появится ссылка на Spotify playlist +- `GET /health` should return `{"ok": true}` +- after `/generate`, Telegram should send a Spotify playlist link -## Типичный деплой +## Typical deployment - VPS + Docker Compose -- `APP_BASE_URL` = публичный URL сервиса +- `APP_BASE_URL` = public service URL - `SPOTIFY_REDIRECT_URI` = `${APP_BASE_URL}/auth/spotify/callback` -- Telegram работает через polling (webhook не нужен) -- `cron` можно не включать совсем, если генерация только вручную +- Telegram runs via polling (no webhook required) +- `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 -Ниже план ближайших улучшений (roadmap), которые хорошо ложатся на текущую архитектуру. +Roadmap items that fit the current architecture well: -- Явный feedback loop: - - команды вроде `/ban`, `/unban`, `/prefer` - - отдельная blacklist-таблица, чтобы "не понравилось" != "просто не лайкнул" -- Настройки anti-repeat: - - жесткий запрет повторов на N дней/недель - - отдельные правила для liked / previously recommended +- Explicit feedback loop: + - commands like `/ban`, `/unban`, `/prefer` + - separate blacklist table so "didn't like it" != "just didn't save it" +- Anti-repeat controls: + - hard no-repeat window (N days/weeks) + - separate rules for liked / previously recommended tracks - Explainability / debug: - - why-this-track (показать источник, score, причины попадания) - - dry-run endpoint/команда без создания плейлиста -- Тонкая настройка алгоритма: - - веса источников (Spotify / Last.fm / search fallback) - - режимы генерации (explore / familiar / mixed) -- Улучшение источников кандидатов: - - дополнительные музыкальные источники / метаданные - - более умная работа с жанрами/артист-кластерами -- Персональный scheduler: - - per-user timezone и per-user cron schedule - - выбор дней недели / времени генерации -- Наблюдаемость: - - структурированные логи по source coverage и причинам фильтрации - - простые метрики по ошибкам Spotify/Last.fm и latency -- Хранилище / масштабирование: - - миграции (Alembic) - - Postgres вместо SQLite для multi-user сценариев + - why-this-track (source, score, reasons) + - dry-run endpoint/command without creating a playlist +- Fine-tuning the algorithm: + - source weights (Spotify / Last.fm / search fallback) + - generation modes (explore / familiar / mixed) +- Better candidate sources: + - additional music metadata sources + - smarter genre/artist clustering +- Personal scheduler: + - per-user timezone and per-user cron schedule + - weekday / time selection +- Observability: + - structured logs for source coverage and filtering reasons + - basic metrics for Spotify/Last.fm errors and latency +- Storage / scaling: + - migrations (Alembic) + - Postgres instead of SQLite for multi-user usage -## Ограничения / улучшения (если захотите дальше) +## Limitations / future improvements -- Персонификация по timezone на пользователя (сейчас cron общий, но user-specific generation поддерживается вручную) -- Больше источников похожих треков (например, MusicBrainz/Discogs mapping) -- Выделенный Postgres вместо SQLite для multi-user нагрузки +- Per-user timezone support is only partially used today (cron is global, though manual per-user generation is supported) +- More candidate sources could improve quality (e.g. MusicBrainz/Discogs mapping) +- Postgres would be better than SQLite for higher multi-user load