Files
spotify_vibe/DESIGN.md
2026-02-26 19:33:05 +00:00

12 KiB
Raw Blame History

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 <INTERNAL_JOB_TOKEN>

Слои приложения

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) могут различаться между приложениями/аккаунтами