Files
radiola-backend/src/now-playing/roks-now-playing.service.ts
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

98 lines
3.4 KiB
TypeScript
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.

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<string, TavrTrack>();
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<TavrTrack[] | null> {
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;
}
}
}