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

303 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 нагрузки