303 lines
14 KiB
Markdown
303 lines
14 KiB
Markdown
# Spotify Daily Vibe Bot (Telegram + Spotify + Docker)
|
||
|
||
Готовый backend-сервис, который:
|
||
|
||
- привязывается к вашему Spotify-аккаунту
|
||
- читает лайкнутые треки (`Liked Songs`)
|
||
- учитывает недавние прослушивания (последние дни)
|
||
- генерирует Spotify-плейлист с похожим вайбом по команде `/generate`
|
||
- опционально может запускаться по расписанию через `cron`
|
||
- минимизирует повторы и старается держать `>=80%` новых треков (не лайкнутых и не рекомендованных ранее ботом)
|
||
- управляется через Telegram
|
||
- запускается в Docker (`app`, опционально `cron`)
|
||
|
||
## Что внутри
|
||
|
||
- `FastAPI` backend (OAuth callback + internal job endpoint)
|
||
- `python-telegram-bot` (polling)
|
||
- `SQLite` (история рекомендаций, кэш лайкнутых, run log)
|
||
- `supercronic` в отдельном контейнере для nightly cron trigger (опционально)
|
||
|
||
## Важный момент по Spotify API
|
||
|
||
Spotify endpoint `/recommendations` может быть ограничен/недоступен для некоторых приложений. В сервисе реализован fallback:
|
||
|
||
- Spotify recommendations (если доступен)
|
||
- top tracks по артистам из вашего recent listening / liked library
|
||
- Spotify search по seed-артистам (если recommendations/top-tracks недоступны)
|
||
- optional Last.fm similarity (очень желательно для лучшего "вайба")
|
||
|
||
Для лучшего качества рекомендаций рекомендуется добавить `LASTFM_API_KEY`.
|
||
|
||
## Быстрый старт
|
||
|
||
1. Создайте Telegram-бота через `@BotFather` и получите токен.
|
||
2. Создайте Spotify App: https://developer.spotify.com/dashboard
|
||
3. В Spotify App добавьте Redirect URI (должен совпасть 1 в 1), например:
|
||
- `https://your-domain.com/auth/spotify/callback`
|
||
- или для локальной разработки через tunnel: `https://xxxx.ngrok-free.app/auth/spotify/callback`
|
||
4. Скопируйте `.env.example` в `.env` и заполните значения.
|
||
5. Запустите:
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
По умолчанию это поднимет только `app` (ручной режим через Telegram `/generate`).
|
||
|
||
Если захотите включить ночной `cron`, запустите отдельно:
|
||
|
||
```bash
|
||
docker compose --profile cron up -d cron
|
||
```
|
||
|
||
6. Откройте Telegram и напишите боту:
|
||
- `/start`
|
||
- `/connect` (получите ссылку на Spotify auth)
|
||
- после подключения: `/generate`
|
||
|
||
## Настройка `.env`
|
||
|
||
Минимально обязательные поля:
|
||
|
||
- `TELEGRAM_BOT_TOKEN`
|
||
- `SPOTIFY_CLIENT_ID`
|
||
- `SPOTIFY_CLIENT_SECRET`
|
||
- `SPOTIFY_REDIRECT_URI`
|
||
- `INTERNAL_JOB_TOKEN`
|
||
|
||
Рекомендуемые:
|
||
|
||
- `LASTFM_API_KEY` (улучшает похожесть треков)
|
||
- `APP_TIMEZONE` / `TZ`
|
||
- `SPOTIFY_DEFAULT_MARKET` (двухбуквенный код страны, например `NL`, `DE`, `US`)
|
||
- `CRON_SCHEDULE` (например `15 2 * * *`, только если включаете `cron`)
|
||
|
||
## Telegram команды
|
||
|
||
- `/connect` — привязать Spotify
|
||
- `/status` — статус подключения и последний плейлист
|
||
- `/generate` — сгенерировать плейлист сейчас
|
||
- `/latest` — ссылка на последний плейлист
|
||
- `/setsize 30` — размер плейлиста (5..100)
|
||
- `/setratio 0.8` — целевая доля новых треков (0.5..1.0)
|
||
- `/sync` — принудительно обновить лайкнутые треки
|
||
|
||
## Алгоритм подбора рекомендаций
|
||
|
||
Ниже описан фактический пайплайн генерации плейлиста (как он сейчас работает в коде).
|
||
|
||
### 1. Подготовка входных данных
|
||
|
||
Перед генерацией бот:
|
||
|
||
- обновляет Spotify access token по refresh token (если нужно)
|
||
- синхронизирует лайкнутые треки из `Liked Songs` в локальный кэш (`saved_tracks`)
|
||
- загружает recent listening за окно `RECENT_DAYS_WINDOW` (по умолчанию `5` дней)
|
||
- загружает историю ранее рекомендованных треков (`recommendation_history`)
|
||
|
||
### 2. Построение seed-профиля
|
||
|
||
Бот собирает seed'ы из двух источников: recent plays и liked library.
|
||
|
||
- Recent plays:
|
||
- каждый recent track получает вес с убыванием по позиции (более свежие прослушивания важнее)
|
||
- накапливаются веса по трекам и артистам
|
||
- Liked tracks:
|
||
- берется срез последних лайков (`~120`)
|
||
- плюс случайная выборка из более старых лайков (для разнообразия)
|
||
- из них также накапливаются веса по артистам
|
||
|
||
На выходе seed-профиля формируются:
|
||
|
||
- `seed_track_ids` (до ~10 треков)
|
||
- `seed_artists` (до ~20 артистов)
|
||
- `seed_artist_names` (для Last.fm и Spotify Search fallback)
|
||
- `recent_track_meta` (нужно для Last.fm track-similar)
|
||
|
||
### 3. Сбор кандидатов (candidate pool)
|
||
|
||
Бот собирает общий пул кандидатов из нескольких источников и дедуплицирует их.
|
||
|
||
Источники (по порядку):
|
||
|
||
1. `Spotify recommendations`
|
||
- вызывается батчами
|
||
- соблюдается лимит Spotify: максимум `5` seed'ов на запрос (суммарно track + artist)
|
||
2. `Spotify artist top tracks`
|
||
- по seed-артистам
|
||
3. `Spotify search` по seed-артистам (fallback)
|
||
- используется, если recommendations / top-tracks ограничены или дали мало результатов
|
||
4. `Last.fm track similar` -> `Spotify search`
|
||
- для recent seed-треков
|
||
5. `Last.fm artist similar` -> `Spotify search`
|
||
- для seed-артистов
|
||
|
||
Если Spotify/Last.fm отдают ошибки на отдельных вызовах, бот старается деградировать мягко (использовать другие источники), а не валить весь run сразу.
|
||
|
||
### 4. Дедупликация кандидатов
|
||
|
||
Кандидаты дедуплицируются:
|
||
|
||
- по `spotify_track_id`
|
||
- по нормализованной сигнатуре `track_name + artist_names` (на случай дублей / разных версий)
|
||
|
||
Если один и тот же трек найден из нескольких источников:
|
||
|
||
- сохраняется лучший score
|
||
- источник объединяется (например, `source1+source2`)
|
||
|
||
### 5. Фильтрация и ранжирование
|
||
|
||
Базовая логика:
|
||
|
||
- сначала исключаются треки, которые уже есть в ваших лайках (`liked_ids`)
|
||
- если после этого пул пустой, включается fallback:
|
||
- разрешается использовать already-liked треки (с penalty), чтобы не падать с пустым результатом
|
||
|
||
Дополнительные коррекции score:
|
||
|
||
- penalty за уже рекомендованные раньше ботом (`history_ids`)
|
||
- penalty за лайкнутые (если включился liked fallback)
|
||
- небольшой boost за коллаборации / нескольких артистов
|
||
- небольшой boost за накопленные причины/источники
|
||
- popularity scoring слегка тяготеет к mid-popularity (не только мейнстрим и не только deep cuts)
|
||
|
||
### 6. Отбор финального списка (selection)
|
||
|
||
После ранжирования кандидаты делятся на:
|
||
|
||
- `novel` — не были рекомендованы ранее и не в лайках
|
||
- `reused` — уже были рекомендованы или (fallback) уже лайкнуты
|
||
|
||
Далее бот:
|
||
|
||
- сначала пытается набрать минимум по `min_new_ratio`
|
||
- соблюдает artist caps (ограничение количества треков одного артиста)
|
||
- если новых треков недостаточно, ослабляет ограничения
|
||
- затем дозаполняет повторными кандидатами
|
||
|
||
Результат:
|
||
|
||
- `tracks` — финальный порядок треков
|
||
- `new_count` / `reused_count`
|
||
- `notes` — пояснение, если не удалось выдержать target по новым трекам
|
||
|
||
### 7. Создание плейлиста и запись истории
|
||
|
||
После сборки списка бот:
|
||
|
||
- создает Spotify playlist
|
||
- добавляет треки
|
||
- записывает run в `playlist_runs` и `playlist_run_tracks`
|
||
- обновляет `recommendation_history`
|
||
- сохраняет `latest_playlist_url` у пользователя
|
||
|
||
## Как работает анти-повтор
|
||
|
||
Бот хранит:
|
||
|
||
- все треки, которые уже рекомендовал раньше
|
||
- все ваши лайкнутые треки (кэш обновляется)
|
||
|
||
При сборке нового плейлиста:
|
||
|
||
- сначала исключает лайкнутые треки (если это возможно)
|
||
- отдает приоритет трекам, которых не было в рекомендациях ранее
|
||
- если новых треков не хватает, дозаполняет повторами из истории
|
||
- если кандидаты есть только среди лайкнутых, может использовать liked fallback вместо полного фейла run
|
||
- пишет статистику в БД (`new / reused`)
|
||
|
||
Если в доступном пуле не хватает новых треков для `80%`, бот сообщит об этом в статусе run.
|
||
|
||
## Cron (ночной запуск)
|
||
|
||
По умолчанию `cron` отключен (manual-first режим: запускаете `/generate` вручную в Telegram).
|
||
|
||
В `docker-compose.yml` сервис `cron` помечен профилем `cron`, поэтому он не стартует при обычном:
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Если хотите включить ночной запуск, поднимите его отдельно:
|
||
|
||
```bash
|
||
docker compose --profile cron up -d cron
|
||
```
|
||
|
||
`cron` по `CRON_SCHEDULE` вызывает внутренний endpoint:
|
||
|
||
- `POST /internal/jobs/nightly`
|
||
|
||
Измените время через `.env`:
|
||
|
||
```env
|
||
CRON_SCHEDULE=15 2 * * *
|
||
TZ=Europe/Moscow
|
||
```
|
||
|
||
Отключить обратно:
|
||
|
||
```bash
|
||
docker compose stop cron
|
||
```
|
||
|
||
## Хранилище данных
|
||
|
||
- SQLite БД: `./data/app.db`
|
||
|
||
Эта папка примонтирована как volume, поэтому данные переживают перезапуск контейнеров.
|
||
|
||
## Проверка работы
|
||
|
||
- `GET /health` должен вернуть `{"ok": true}`
|
||
- после `/generate` в Telegram появится ссылка на Spotify playlist
|
||
|
||
## Типичный деплой
|
||
|
||
- VPS + Docker Compose
|
||
- `APP_BASE_URL` = публичный URL сервиса
|
||
- `SPOTIFY_REDIRECT_URI` = `${APP_BASE_URL}/auth/spotify/callback`
|
||
- Telegram работает через polling (webhook не нужен)
|
||
- `cron` можно не включать совсем, если генерация только вручную
|
||
|
||
## Архитектура
|
||
|
||
Подробное описание архитектуры приложения, потоков данных и таблиц БД вынесено в `DESIGN.md`.
|
||
|
||
## Feature Plans
|
||
|
||
Ниже план ближайших улучшений (roadmap), которые хорошо ложатся на текущую архитектуру.
|
||
|
||
- Явный feedback loop:
|
||
- команды вроде `/ban`, `/unban`, `/prefer`
|
||
- отдельная blacklist-таблица, чтобы "не понравилось" != "просто не лайкнул"
|
||
- Настройки anti-repeat:
|
||
- жесткий запрет повторов на N дней/недель
|
||
- отдельные правила для liked / previously recommended
|
||
- 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 сценариев
|
||
|
||
## Ограничения / улучшения (если захотите дальше)
|
||
|
||
- Персонификация по timezone на пользователя (сейчас cron общий, но user-specific generation поддерживается вручную)
|
||
- Больше источников похожих треков (например, MusicBrainz/Discogs mapping)
|
||
- Выделенный Postgres вместо SQLite для multi-user нагрузки
|