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; + } + } +}