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