# Design / Architecture ## Цель Сервис генерирует Spotify playlist "daily vibe" на основе: - recent listening пользователя - кэша liked tracks - истории уже выданных рекомендаций Управление идет через Telegram-бота (`/generate`, `/connect`, `/status` и т.д.), а также опционально через nightly cron trigger. ## Высокоуровневая схема Компоненты: - `FastAPI` приложение - healthcheck - Spotify OAuth start/callback - internal endpoint для cron (`/internal/jobs/nightly`) - `TelegramBotRunner` (polling) - принимает команды пользователей - запускает генерацию и отправляет статусы - `PlaylistJobService` - orchestration одного run (token -> sync likes -> candidates -> playlist -> persist) - `RecommendationEngine` - строит seed profile - собирает candidate pool - ранжирует и отбирает треки - `SpotifyClient` / `LastFmClient` - внешние API вызовы - `SQLite` (через SQLAlchemy async) - пользователи, кэш лайков, история рекомендаций, run log ## Runtime / Lifecycle Точка входа: `app/main.py`. На startup: 1. Загружается `Settings` (`app/config.py`) 2. Создается async SQLAlchemy engine и session factory (`app/db/session.py`) 3. Выполняется `create_all` (автосоздание таблиц) 4. Создается общий `httpx.AsyncClient` 5. Создаются клиенты: - `SpotifyClient` - `LastFmClient` 6. Создаются сервисы: - `SpotifyAuthService` - `RecommendationEngine` - `PlaylistJobService` 7. Инициализируется `TelegramBotRunner` и запускается polling 8. Все объекты складываются в `app.state.runtime` и `app.state.services` На shutdown: - останавливается Telegram polling - закрывается `httpx.AsyncClient` - закрывается DB engine ## Контейнеры / Deployment `docker-compose.yml`: - `app` (основной сервис, FastAPI + Telegram polling) - `cron` (опциональный сервис с `supercronic`) Важно: - `cron` помечен `profiles: ["cron"]` и по умолчанию не стартует - manual-first режим: пользователь генерирует плейлисты через Telegram `/generate` `cron` выполняет `scripts/run_nightly.sh`, который вызывает: - `POST /internal/jobs/nightly` с `Authorization: Bearer ` ## Слои приложения ### 1. API Layer (`app/api/routes.py`) Назначение: - HTTP endpoints для OAuth и internal jobs Endpoints: - `GET /health` - `GET /auth/spotify/start` - `GET /auth/spotify/callback` - `POST /internal/jobs/nightly` Особенности: - OAuth callback после успеха отправляет сообщение в Telegram пользователю - internal nightly endpoint защищен `INTERNAL_JOB_TOKEN` ### 2. Bot Layer (`app/bot/telegram_bot.py`) Назначение: - пользовательский интерфейс через Telegram команды Поддерживаемые команды: - `/start` - `/help` - `/connect` - `/status` - `/generate` - `/latest` - `/setsize` - `/setratio` - `/sync` Особенности: - `/generate` запускает `PlaylistJobService.generate_for_user(..., force=True, notify=False)` - `/sync` только обновляет кэш лайков - у каждой команды свой короткий DB session через `session_factory` ### 3. Service Layer #### `SpotifyAuthService` (`app/services/spotify_auth.py`) Роли: - создание OAuth state - обмен `code` на токены - refresh access token - защита от истекшего access token Особенности: - сравнение дат нормализуется в UTC (важно для SQLite naive datetime) - сохраняет scopes и expiry в таблице `users` #### `RecommendationEngine` (`app/services/recommendation.py`) Роли: - sync liked tracks в локальный кэш - build seed profile - collect candidates из нескольких источников - rank/select итоговый список Текущие источники кандидатов: - Spotify recommendations - Spotify artist top tracks - Spotify search (seed artist fallback) - Last.fm track similar -> Spotify search - Last.fm artist similar -> Spotify search Ключевые особенности: - соблюдение лимита Spotify recommendations: максимум `5` seed'ов на запрос - мягкая деградация при частичных ошибках источников - liked fallback (если весь пул оказался уже в лайках) #### `PlaylistJobService` (`app/services/playlist_job.py`) Роли: - orchestration полного run - создание Spotify playlist и добавление треков - запись run и треков в БД - обновление recommendation history - отправка уведомления в Telegram (если задан notifier) Порядок выполнения run: 1. Проверка пользователя / Spotify connection 2. Создание записи `playlist_runs` со статусом `running` 3. Получение valid access token 4. Sync liked tracks 5. Сборка плейлиста через `RecommendationEngine` 6. Создание playlist в Spotify 7. Добавление треков в playlist 8. Сохранение run-трека/истории/метаданных 9. Commit и возврат `JobOutcome` При ошибке: - `playlist_runs.status = failed` - в `notes` записывается сообщение ошибки ## Client Layer ### `SpotifyClient` (`app/clients/spotify.py`) Инкапсулирует Spotify Web API. Что важно в текущей реализации: - `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` ### `LastFmClient` (`app/clients/lastfm.py`) Используется как optional enrichment layer. - может быть отключен (если `LASTFM_API_KEY` пустой) - ошибки Last.fm не должны ломать весь run, если другие источники работают ## Persistence Layer (SQLite + SQLAlchemy) ### Таблицы (`app/db/models.py`) #### `users` Хранит: - 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`) #### `auth_states` Временные OAuth state для callback: - `state` - `telegram_chat_id` - `expires_at` #### `saved_tracks` Локальный кэш `Liked Songs` пользователя: - `spotify_track_id` - название/артисты/album/popularity - `added_at` #### `recommendation_history` История ранее рекомендованных треков: - `spotify_track_id` - `first_recommended_at` - `last_recommended_at` - `times_recommended` #### `playlist_runs` Run log генерации: - статус (`running/success/failed`) - метаданные Spotify playlist - статистика (`total/new/reused`) - `notes` #### `playlist_run_tracks` Снимок состава конкретного run: - track id / name / artists - source (из какого источника пришел) - позиция - `is_new_to_bot` ### Repository Layer (`app/db/repositories.py`) Паттерн: - thin repositories над SQLAlchemy AsyncSession - изолируют CRUD/query-логику от service layer Примеры: - `UserRepository` - `AuthStateRepository` - `SavedTrackRepository` - `RecommendationHistoryRepository` - `PlaylistRunRepository` ## Потоки данных ### OAuth Flow 1. Telegram `/connect` 2. `SpotifyAuthService.create_connect_url()` 3. Пользователь идет в Spotify auth page 4. `GET /auth/spotify/callback` 5. `SpotifyAuthService.handle_callback()` 6. Токены и Spotify profile сохраняются в `users` 7. Пользователю отправляется сообщение в Telegram ### Manual Generate 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 5. Persist run/history 6. Ответ пользователю в Telegram ### Nightly Cron Flow (optional) 1. `supercronic` в `cron` контейнере 2. `scripts/run_nightly.sh` 3. `POST /internal/jobs/nightly` 4. `PlaylistJobService.generate_for_all_connected_users()` ## Concurrency / Consistency - Генерация защищена одним `asyncio.Lock` (`generate_lock`) в `PlaylistJobService` - предотвращает одновременные run'ы и гонки обновления history - Большинство операций run выполняются в одной DB session - Ошибки внутри run переводят запись run в `failed` ## Алгоритм рекомендаций (кратко) Подробно см. `README.md`, но архитектурно pipeline такой: 1. Seed profile (recent + liked) 2. Candidate pool (Spotify + Last.fm + fallback search) 3. Dedupe 4. Rank (score penalties/boosts) 5. Select (min_new_ratio + artist caps) 6. Persist stats/history ## Конфигурация Основные env-переменные (`app/config.py`): - `TELEGRAM_BOT_TOKEN` - `SPOTIFY_CLIENT_ID` - `SPOTIFY_CLIENT_SECRET` - `SPOTIFY_REDIRECT_URI` - `SPOTIFY_DEFAULT_MARKET` - `LASTFM_API_KEY` (optional) - `INTERNAL_JOB_TOKEN` - `DB_PATH` - `DEFAULT_PLAYLIST_SIZE` - `MIN_NEW_RATIO` - `RECENT_DAYS_WINDOW` - `PLAYLIST_VISIBILITY` ## Диагностика / Наблюдаемость Сейчас: - основной feedback идет через Telegram сообщения и `playlist_runs.notes` - HTTP `/health` для liveness - тесты покрывают критичные Spotify routes и части recommendation pipeline Что можно улучшить: - структурированные логи по source coverage (сколько кандидатов из каждого источника) - метрики latency/ошибок Spotify/Last.fm - отдельный debug endpoint для dry-run (без создания playlist) ## Известные ограничения - SQLite подходит для small-scale / single-node сценария - Telegram polling + FastAPI живут в одном процессе/контейнере - per-user timezone используется ограниченно (cron общий) - внешние API ограничения (Spotify/Last.fm) могут различаться между приложениями/аккаунтами