Commit Graph

39 Commits

Author SHA1 Message Date
nk
4aa3b55b5e perf(backend): ретенция track_plays, прун сирот-треков, проекция now-playing, пул БД
- MaintenanceService (@Cron daily 4:00): ретенция track_plays >180д чанками по 20k
  (без ретенции таблица растёт ~100k строк/сутки) + прун осиротевших треков
  (без проигрываний/лайков/обложки, >30д). Сейчас удаляет 0 (данным 4 дня) —
  только ограничивает будущий рост. ВНИМАНИЕ: 180д ограничивает и чарт period=all.
- getAllNowPlaying: select-проекция (stationId+name) вместо include station:true —
  не тянем всю строку Station (streamUrl, tags[], даты) на каждый ряд now_playing.
- PrismaService: connection_limit=20 в URL идемпотентно (дефолт ~5 мал под ~16
  конкурентных поллеров).
2026-06-06 17:08:36 +03:00
nk
944ec63df0 refactor(now-playing): единый IcyReader + реестр dedicated-источников
Убраны 3 копии state-machine разбора icy-metaint (icy/novoeby/love) → один
readIcyStreamTitle в icy-reader.ts (с опцией decode auto-1251 для cp1251-потоков
и корректным терминатором ';' — апострофы в названиях больше не обрезаются).

Ручной genre-notIn список в IcyNowPlayingService заменён центральным реестром
dedicated-sources.ts (host + genre), согласованным с селекторами самих сервисов:
добавил выделенный сервис — впиши host/genre в одно место, ICY его пропустит
автоматически. Исключение по хосту провабельно совпадает с тем, что сервис
реально обрабатывает (раньше genre легко забывали добавить).
2026-06-06 16:54:02 +03:00
nk
a3434ed894 perf(backend): индексы, кэш чартов, пропуск upsert, фикс N+1 обогащения
- track_plays(played_at,track_id,station_id) покрывающий + tracks(first_seen_at) WHERE
  pending частичный (применены CONCURRENTLY на проде + миграция idempotent)
- ChartsService.getTopTracks: in-memory TTL-кэш 90с по (period,genre,limit) → детальная
  страница и параллельные запросы не пересчитывают тяжёлые агрегации
- NowPlayingService.ingest: не пишем now_playing и не шлём сокет, если трек не изменился
  (было ~20k бесполезных upsert/час)
- enrichNowPlaying: вместо N+1 (300 upsert/мин) — один batched findMany по normKey

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:20:25 +03:00
nk
38e380a59f fix(now-playing): Radio 7 главные каналы — slug не съедает '7'
Баг: strip /\d+$/ у маунта radio7128 резал '7128' → 'radio' (пусто в meta).
Теперь slug = radio7(_слово)? без битрейта: radio7128→radio7, radio7_love64→radio7-love.
2026-06-06 09:50:18 +03:00
nk
cc30422d8d feat(now-playing): 101.ru (Comedy Radio + Радио Energy) и Radio 7 через EMG
101.ru (Comedy, NRJ/Energy, ~15 каналов): id канала = последний сегмент потока
pub*.101.ru/.../{id}; трек GET 101.ru/api/channel/getTrackOnAir/{id}/?idcity=1 →
result.short {titleExecutorFull, titleTrack, cover.coverOriginal}; обложка cdn0.101.ru.
Radio 7 — это ЕМГ на старых мейнах radio7.hostingradio.ru: расширил EmgNowPlayingService
(slug radio7128→radio7, radio7_love64→radio7-love). Три жанра исключены из ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 09:29:41 +03:00
nk
c4c475544a feat(now-playing): Радио Ваня + Русская Волна; Питер объединён в SpbRadio
Радио Ваня (20 каналов) — тот же движок/API, что Питер ФМ (один разработчик):
объединил в SpbRadioNowPlayingService (NETWORKS=[piterfm, radiovanya]), матч
станции по МАУНТУ из поля link (у Вани slug≠маунт). Обложки iTunes.
Русская Волна (~27, amgradio.ru, ICY нет) — VolnaNowPlayingService: единый
info.volna.top/radio.json, поля {prefix}_title, маунт→префикс (RusRock128→rusrock,
ChillaFM128→chilla). Обложки через обогащение. Оба жанра исключены из ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 09:18:23 +03:00
nk
3c4f349f71 fix(now-playing): Орфей — чиним кодировку (двойная мойибейк cp1251) + режем хекс-хвосты
status-json смешанной кодировки: часть тайтлов нормальный UTF-8, часть — cp1251-
байты, прочитанные как latin1 и завёрнутые в UTF-8 (мойибейк «Íèêîëà»→«Никола»).
fixEncoding: реальную кириллицу не трогаем, мойибейк (À-ÿ) восстанавливаем
latin1→windows-1251. Срезаем приклеенный служебный id трека (- f0098627), фильтр
hex/числовых плейсхолдеров усилен. Каналы без реального произведения — без подписи.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 09:01:03 +03:00
nk
c87a0caa5c feat(now-playing): Питер ФМ и Орфей
Питер ФМ (16 каналов, cdnvideo) — ICY пуст; берём трек+обложку из их API
radiopiterfm.ru: /api/v1/streams/ (slug↔id) + /api/v5/playlists/{id}/ →
items[0].track {name, artist.name, imglarge}. Обложки готовые (iTunes).
Орфей (классика, radio.orpheus.ru) — через Icecast status-json.xsl по маунтам
(Chan_N), парсим «Композитор — Произведение», отсекаем мусор (hex/URL/undefined);
обложка через обогащение. Оба жанра исключены из общего ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:34:32 +03:00
nk
426fd0e197 feat(now-playing): Новое Радио BY — now-playing с правильной кодировкой + Wake Up
5 мейнов live.novoeradio.by отдают ICY, но кириллица в windows-1251 (общий
поллер читал UTF-8 → каша). NovoeByNowPlayingService (@Interval 30с): свой ICY-
ридер с декодом UTF-8→fallback 1251, джинглы без трека пропускаем, обложка через
обогащение. Исключён из общего ICY-поллера. Маунт Wake Up исправлен
(wakeupshow→wakeup, был 404/offline) в seed и прод-БД.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:20:06 +03:00
nk
d5f30cd05d fix(now-playing): ГУСЬ — не использовать битый art AzuraCast, обложки через обогащение
song.art (/api/station/{slug}/art/{hash}) на radiogoose.ru отдаёт 404 — route
обложек не включён. Передаём coverUrl=null → обложку подтянет iTunes/Deezer по
normKey, как у ICY-станций.
2026-06-05 20:36:45 +03:00
nk
d8b6a6024f fix(now-playing): ГУСЬ Технорейв — алиас slug harddance→technorave
У этого канала mount потока (harddance) не совпадает с ключом AzuraCast API
(technorave). Остальные 15 — совпадают.
2026-06-05 20:27:01 +03:00
nk
cb0e401854 feat(now-playing): ГУСЬ (radiogoose) — now-playing + обложки, починка потоков
Сеть ГУСЬ (16 каналов) на AzuraCast (radiogoose.ru). Потоки в каталоге были
многострочными (url1\nurl2) → клиент рвал воспроизведение, health-check метил
offline, станции скрывались. Почищены на канонический https://radiogoose.ru/listen/{slug}/play.
GooseNowPlayingService (@Interval 30с): AzuraCast API /api/nowplaying/{slug} →
now_playing.song {artist,title,art}, ingest + isOnline=true. Исключён из ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:22:28 +03:00
nk
a06a9b2a2b feat(now-playing): now-playing + обложки для Зайцев ФМ
19 каналов Зайцев ФМ (MP3 abs.zaycev.fm) не отдают ICY → трека не было.
ZaycevNowPlayingService (@Interval 30с): тянет текущий трек из API сайта
GET https://www.zaycev.fm/api/v1/recent?channel={slug}&limit=1 (slug = буквенная
часть имени потока, pop256k→pop), берёт artist/title и готовую обложку
(radio2.zaycev.fm/artistimages), фильтр is_music. Зайцев исключён из ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:52:37 +03:00
nk
4d9fd24074 feat(now-playing): now-playing + обложки для каналов Unistar
8 HLS-каналов Unistar (Беларусь) не отдают ICY, поэтому трек брали неоткуда.
Добавлен UnistarNowPlayingService (@Interval 30с): тянет текущий трек из их API
https://api3.unistar.by/client/latest/{slug} (slug = сегмент /hls/{slug}/ потока),
берёт Artist/Title и имя файла обложки (unistar.by/upload/music/photos/), ingest'ит.
Только Type=Music. Unistar исключён из ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:37:17 +03:00
nk
3c6dbed659 feat(now-playing): Radio ROKS через TavR Media API (трек + обложки)
Главный канал ROKS не отдаёт трек по ICY (StreamTitle пустой), сабканалы —
без обложек. Новый RoksNowPlayingService опрашивает o.tavr.media/roks
(главный) и /roks4songs (сабканалы по type ukr/bal/new/har), отдаёт и трек,
и обложку static.radioroks.ua. Исключил genre='Radio ROKS' из ICY-поллера.
2026-06-04 11:44:30 +03:00
nk
51576f7198 feat(now-playing): Радио Монте-Карло через Крутой Медиа API
Все 21 канал Монте-Карло — сеть Крутой Медиа (dfm.ru/api/n/current).
Добавил genre='Radio Monte Carlo' в DfmNowPlayingService, матчинг по
слагу из маута потока (basename без битрейта: blues96.aacp -> blues),
исключил из ICY-поллера. Чинит 5 каналов, залипших на 'Дух — Тишина'
(Blues, Chill Lounge, Italiano, Meditation, Summertime).
2026-06-04 10:55:09 +03:00
nk
588857a73e feat(now-playing): MAXIMUM через тот же Крутой-API (dfm.ru/api/n/current)
MAXIMUM — сеть Крутой Медиа, её каналы в том же /api/n/current. Поллер расширен
на genre IN (DFM, MAXIMUM) + алиасы (maxbritpop, max80, maximum90, rockhits и т.д.).
ICY исключает MAXIMUM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:36:48 +03:00
nk
bd2cd36f1e fix(now-playing): Love Radio — ICY авторизованных n340-потоков (per-channel)
player/online кэширует один трек на все каналы. Берём трек из ICY самих потоков
(каждый поток физически разный), читая их с сессионным UID (бэкенд берёт из config).
Теперь у каждого Love-канала свой трек.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:09:01 +03:00
nk
68c919c8ba fix(now-playing): Love Radio — online?musicStreamId (per-channel), не history
history/list игнорирует musicStreamId (всегда главный эфир) → все каналы показывали
один трек. player/online?musicStreamId отдаёт верный трек канала.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:23:56 +03:00
nk
fa7742d06e feat(now-playing): Love Radio через api.loveradio.ru (ICY шлёт мусор onlinestop56k)
ICY-потоки Love Radio отдают 'onlinestop56k' вместо трека. Берём текущий трек из
их API (player/history/list?musicStreamId=N&limit=1, data[0]). Статичный маппинг
наших станций -> musicStreamId. ICY-поллер исключает genre='Love Radio'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:19:12 +03:00
nk
7ff48fff29 fix(now-playing): EMG-фолбэк slug europaplus-{x} (Fresh застрял)
Slug из хоста потока не всегда = meta-slug: hls-01-fresh → meta это europaplus-fresh.
Если по основному slug пусто — пробуем europaplus-{slug}. Fresh теперь обновляется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:26:51 +03:00
nk
7e6b0c8dc6 feat(now-playing): DFM/Крутой Медиа через dfm.ru/api/n/current
Сабканалы DFM (Skrillex, Daft Punk, K-Pop, Игромания и др.) не отдают ICY-метаданные.
Единый веб-API dfm.ru/api/n/current даёт текущий трек + WebP-обложку по всем ~147
каналам (ключ slug). DfmNowPlayingService матчит наши DFM-станции по нормализованному
имени (+ числовой префикс, + алиасы для годов/Игромании/Pioneer). ICY-поллер
исключает genre=DFM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:24:33 +03:00
nk
3215dd5a4e fix(now-playing): отсекать JSON-статус в ICY StreamTitle (101.ru)
Потоки 101.ru (Comedy Club, StandUp, Женский StandUp и др.) шлют в StreamTitle
JSON {"status":1,...} вместо трека — он попадал в now_playing как название.
ICY-парсер и ingest теперь отсекают значения, начинающиеся с { или [.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:11:26 +03:00
nk
499863744f fix(now-playing): EMG-поллер не фильтрует по isOnline + чинит ошибочный offline
health-check ошибочно метит HLS-потоки emgsound как offline → поллер их пропускал.
Теперь поллим все emgsound-станции и при успешном получении трека ставим isOnline=true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 15:34:05 +03:00
nk
38b2aee26d feat(now-playing): EMG (Европа Плюс и др.) now-playing через meta.hostingradio
Станции группы ЕМГ (emgsound.ru) получают текущий трек + готовую WebP-обложку
из единого meta.hostingradio.ru/emg/{slug}/history (slug из хоста потока,
order=desc → первый = сейчас). Заводится через NowPlayingService.ingest
(чарты + обогащение). ICY-поллер теперь пропускает emgsound (там HLS без ICY).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 15:28:06 +03:00
nk
96fabac7f5 fix(now-playing): резолвить обложку трека на чтении /now-playing
Обложка ICY-станций (DFM) теперь подтягивается из обогащённого трека по normKey
в момент ответа API, а не записи now_playing — появляется сразу после обогащения,
без ожидания следующего опроса станции (~6 мин).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:20:41 +03:00
nk
f379110975 feat(now-playing): DFM и др. ICY-станции — обложки + чарты + ротация
ICY-станции (DFM и пр.) теперь полноценно «как Record»:
- ICY-поллер вызывает recordPlay → треки идут в чарты и обогащаются Discogs,
  откуда берётся обложка (раньше now_playing писался напрямую, мимо чартов)
- обложка now-playing: если источник не дал (ICY всегда null) — подставляем
  обложку обогащённого трека из нашей БД по normKey (NowPlayingService.resolveCover)
- ротация курсора по всем станциям (окно 70) вместо первых 50 по кругу —
  раньше 363 из 413 станций не опрашивались
- общий NowPlayingService.ingest для Record и ICY (дедуп логики)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:08:07 +03:00
nk
24ed44e8ab feat(now-playing): добавить name станции в ответ (для матча обложек на клиенте) 2026-06-03 12:10:04 +03:00
nk
df20e0fac6 feat(now-playing): REST GET /now-playing (ключ — id станции каталога)
Клиенту нужен now-playing с правильным маппингом id (Record now-эндпоинт
использует id now-слотов, не каталога). Отдаём текущие треки по станциям
с stationId = catalog id, чтобы клиент сопоставлял по station.id.
2026-06-03 10:51:49 +03:00
nk
38fe92d695 feat(charts): сбор статистики проигрываний и API чартов
- модели Track / TrackPlay / TrackLike (+ миграция add_charts)
- сбор проигрываний в now-playing-поллере: при смене трека на станции
  пишется TrackPlay (нормализация artist+song -> Track), fire-and-forget
  обогащение через MusicBrainz (album/releaseDate)
- ChartsModule: GET /charts/tracks (период day/week/month/all, ранг, тренд,
  проигрывания, станции, лайки), GET /charts/tracks/:id (метрики, таймлайны
  популярности и лайков по дням, топ станций, isLiked), POST/DELETE like
- OptionalAuthGuard для публичной детальной страницы с опц. userId
2026-06-02 23:40:13 +03:00
nk
bbfec76a7b fix: use Node.js http module for reliable ICY parsing 2026-06-02 20:19:21 +03:00
nk
d0874ae9db fix: parallel ICY polling with batch size 10 and 5s timeout 2026-06-02 20:15:15 +03:00
nk
6b2e02f6c0 chore: warn on ICY errors for debugging 2026-06-02 20:11:35 +03:00
nk
0dee9d56b7 chore: add ICY poll logs 2026-06-02 20:08:17 +03:00
nk
1b0c59264f feat: ICY metadata fallback for non-Record stations 2026-06-02 20:03:25 +03:00
nk
09211dceb5 fix: broadcast stationId as int for Android mapping 2026-06-02 19:52:33 +03:00
nk
d082a1ce07 chore: add debug logs to now-playing polling 2026-06-02 19:40:19 +03:00
nk
7823b17d55 feat: now-playing polling from Record API with station mapping and WebSocket broadcast 2026-06-02 19:31:48 +03:00
nk
8aadd62e3c feat: bootstrap NestJS backend with auth, stations, users, health-check, now-playing 2026-06-02 13:54:00 +03:00