From 3c6dbed6596de59a02bba084ca908debce9c5907 Mon Sep 17 00:00:00 2001 From: nk Date: Thu, 4 Jun 2026 11:44:30 +0300 Subject: [PATCH] =?UTF-8?q?feat(now-playing):=20Radio=20ROKS=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20TavR=20Media=20API=20(=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=20+=20=D0=BE=D0=B1=D0=BB=D0=BE=D0=B6=D0=BA=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Главный канал ROKS не отдаёт трек по ICY (StreamTitle пустой), сабканалы — без обложек. Новый RoksNowPlayingService опрашивает o.tavr.media/roks (главный) и /roks4songs (сабканалы по type ukr/bal/new/har), отдаёт и трек, и обложку static.radioroks.ua. Исключил genre='Radio ROKS' из ICY-поллера. --- src/now-playing/icy-now-playing.service.ts | 10 ++- src/now-playing/now-playing.module.ts | 2 + src/now-playing/roks-now-playing.service.ts | 97 +++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/now-playing/roks-now-playing.service.ts diff --git a/src/now-playing/icy-now-playing.service.ts b/src/now-playing/icy-now-playing.service.ts index 1d93e7b..54b0749 100644 --- a/src/now-playing/icy-now-playing.service.ts +++ b/src/now-playing/icy-now-playing.service.ts @@ -29,7 +29,15 @@ export class IcyNowPlayingService { const where = { recordStationId: null, isOnline: true, - genre: { notIn: ['DFM', 'MAXIMUM', 'Love Radio', 'Radio Monte Carlo'] }, + genre: { + notIn: [ + 'DFM', + 'MAXIMUM', + 'Love Radio', + 'Radio Monte Carlo', + 'Radio ROKS', + ], + }, NOT: { streamUrl: { contains: 'emgsound.ru' } }, }; const total = await this.prisma.station.count({ where }); diff --git a/src/now-playing/now-playing.module.ts b/src/now-playing/now-playing.module.ts index 3b7671a..21c2a04 100644 --- a/src/now-playing/now-playing.module.ts +++ b/src/now-playing/now-playing.module.ts @@ -7,6 +7,7 @@ import { IcyNowPlayingService } from './icy-now-playing.service'; import { EmgNowPlayingService } from './emg-now-playing.service'; import { DfmNowPlayingService } from './dfm-now-playing.service'; import { LoveNowPlayingService } from './love-now-playing.service'; +import { RoksNowPlayingService } from './roks-now-playing.service'; import { ChartsModule } from '../charts/charts.module'; @Module({ @@ -20,6 +21,7 @@ import { ChartsModule } from '../charts/charts.module'; EmgNowPlayingService, DfmNowPlayingService, LoveNowPlayingService, + RoksNowPlayingService, ], exports: [NowPlayingService], }) diff --git a/src/now-playing/roks-now-playing.service.ts b/src/now-playing/roks-now-playing.service.ts new file mode 100644 index 0000000..027a2b8 --- /dev/null +++ b/src/now-playing/roks-now-playing.service.ts @@ -0,0 +1,97 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Interval } from '@nestjs/schedule'; +import { PrismaService } from '../prisma/prisma.service'; +import { NowPlayingService } from './now-playing.service'; + +interface TavrTrack { + singer?: string; + song?: string; + cover?: string; + type?: string; +} + +/** + * Now-playing для Radio ROKS (TavR Media). Главный канал не отдаёт трек по ICY + * (StreamTitle пустой), а сабканалы — без обложек. У TavR есть JSON-API, который + * даёт и трек, и обложку (static.radioroks.ua, 500×500): + * • https://o.tavr.media/roks — главный канал, [0] = текущий трек + * • https://o.tavr.media/roks4songs — сабканалы, по полю type (ukr/bal/new/har) + */ +@Injectable() +export class RoksNowPlayingService { + private readonly logger = new Logger(RoksNowPlayingService.name); + private readonly headers = { 'User-Agent': 'Mozilla/5.0' }; + + // Подстрока в имени нашей станции -> type в roks4songs. + // HD-варианты ловятся теми же правилами (Ballads HD, New Rock HD и т.д.). + private readonly typeByName: { match: RegExp; type: string }[] = [ + { match: /ballads/i, type: 'bal' }, + { match: /hard/i, type: 'har' }, + { match: /new\s*rock/i, type: 'new' }, + { match: /ukrai|укра/i, type: 'ukr' }, + ]; + + constructor( + private readonly prisma: PrismaService, + private readonly nowPlayingService: NowPlayingService, + ) {} + + @Interval(30000) + async pollRoks() { + const stations = await this.prisma.station.findMany({ + where: { genre: 'Radio ROKS' }, + }); + if (stations.length === 0) return; + + const [main, subs] = await Promise.all([ + this.fetchTavr('https://o.tavr.media/roks'), + this.fetchTavr('https://o.tavr.media/roks4songs'), + ]); + if (!main && !subs) return; + + const mainCur = main?.[0]; + // type -> текущий трек сабканала (первый по времени = играющий сейчас) + const subByType = new Map(); + for (const s of subs ?? []) { + if (s.type && !subByType.has(s.type)) subByType.set(s.type, s); + } + + let updated = 0; + for (const station of stations) { + const rule = this.typeByName.find((r) => r.match.test(station.name)); + const cur = rule ? subByType.get(rule.type) : mainCur; + if (!cur?.singer || !cur?.song) continue; + + await this.nowPlayingService.ingest({ + stationDbId: station.id, + stationNumericId: station.stationId, + artist: cur.singer.trim(), + song: cur.song.trim(), + coverUrl: cur.cover || null, + }); + if (!station.isOnline) { + await this.prisma.station.update({ + where: { id: station.id }, + data: { isOnline: true }, + }); + } + updated++; + } + + this.logger.log(`ROKS poll: ${updated}/${stations.length} обновлено`); + } + + private async fetchTavr(url: string): Promise { + try { + const res = await fetch(url, { headers: this.headers }); + if (!res.ok) { + this.logger.warn(`${url} вернул ${res.status}`); + return null; + } + return (await res.json()) as TavrTrack[]; + } catch (e) { + this.logger.warn(`${url}: ${(e as Error).message}`); + return null; + } + } +}