12 KiB
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:
- Загружается
Settings(app/config.py) - Создается async SQLAlchemy engine и session factory (
app/db/session.py) - Выполняется
create_all(автосоздание таблиц) - Создается общий
httpx.AsyncClient - Создаются клиенты:
SpotifyClientLastFmClient
- Создаются сервисы:
SpotifyAuthServiceRecommendationEnginePlaylistJobService
- Инициализируется
TelegramBotRunnerи запускается polling - Все объекты складываются в
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 <INTERNAL_JOB_TOKEN>
Слои приложения
1. API Layer (app/api/routes.py)
Назначение:
- HTTP endpoints для OAuth и internal jobs
Endpoints:
GET /healthGET /auth/spotify/startGET /auth/spotify/callbackPOST /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: максимум
5seed'ов на запрос - мягкая деградация при частичных ошибках источников
- liked fallback (если весь пул оказался уже в лайках)
PlaylistJobService (app/services/playlist_job.py)
Роли:
- orchestration полного run
- создание Spotify playlist и добавление треков
- запись run и треков в БД
- обновление recommendation history
- отправка уведомления в Telegram (если задан notifier)
Порядок выполнения run:
- Проверка пользователя / Spotify connection
- Создание записи
playlist_runsсо статусомrunning - Получение valid access token
- Sync liked tracks
- Сборка плейлиста через
RecommendationEngine - Создание playlist в Spotify
- Добавление треков в playlist
- Сохранение run-трека/истории/метаданных
- 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:
statetelegram_chat_idexpires_at
saved_tracks
Локальный кэш Liked Songs пользователя:
spotify_track_id- название/артисты/album/popularity
added_at
recommendation_history
История ранее рекомендованных треков:
spotify_track_idfirst_recommended_atlast_recommended_attimes_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
Примеры:
UserRepositoryAuthStateRepositorySavedTrackRepositoryRecommendationHistoryRepositoryPlaylistRunRepository
Потоки данных
OAuth Flow
- Telegram
/connect SpotifyAuthService.create_connect_url()- Пользователь идет в Spotify auth page
GET /auth/spotify/callbackSpotifyAuthService.handle_callback()- Токены и Spotify profile сохраняются в
users - Пользователю отправляется сообщение в Telegram
Manual Generate Flow (/generate)
- Telegram
/generate PlaylistJobService.generate_for_user(..., force=True)- Sync likes + recent listening + candidate collection
- Playlist create + add items в Spotify
- Persist run/history
- Ответ пользователю в Telegram
Nightly Cron Flow (optional)
supercronicвcronконтейнереscripts/run_nightly.shPOST /internal/jobs/nightlyPlaylistJobService.generate_for_all_connected_users()
Concurrency / Consistency
- Генерация защищена одним
asyncio.Lock(generate_lock) вPlaylistJobService- предотвращает одновременные run'ы и гонки обновления history
- Большинство операций run выполняются в одной DB session
- Ошибки внутри run переводят запись run в
failed
Алгоритм рекомендаций (кратко)
Подробно см. README.md, но архитектурно pipeline такой:
- Seed profile (recent + liked)
- Candidate pool (Spotify + Last.fm + fallback search)
- Dedupe
- Rank (score penalties/boosts)
- Select (min_new_ratio + artist caps)
- Persist stats/history
Конфигурация
Основные env-переменные (app/config.py):
TELEGRAM_BOT_TOKENSPOTIFY_CLIENT_IDSPOTIFY_CLIENT_SECRETSPOTIFY_REDIRECT_URISPOTIFY_DEFAULT_MARKETLASTFM_API_KEY(optional)INTERNAL_JOB_TOKENDB_PATHDEFAULT_PLAYLIST_SIZEMIN_NEW_RATIORECENT_DAYS_WINDOWPLAYLIST_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) могут различаться между приложениями/аккаунтами