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-поллера.
This commit is contained in:
@@ -29,7 +29,15 @@ export class IcyNowPlayingService {
|
|||||||
const where = {
|
const where = {
|
||||||
recordStationId: null,
|
recordStationId: null,
|
||||||
isOnline: true,
|
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' } },
|
NOT: { streamUrl: { contains: 'emgsound.ru' } },
|
||||||
};
|
};
|
||||||
const total = await this.prisma.station.count({ where });
|
const total = await this.prisma.station.count({ where });
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { IcyNowPlayingService } from './icy-now-playing.service';
|
|||||||
import { EmgNowPlayingService } from './emg-now-playing.service';
|
import { EmgNowPlayingService } from './emg-now-playing.service';
|
||||||
import { DfmNowPlayingService } from './dfm-now-playing.service';
|
import { DfmNowPlayingService } from './dfm-now-playing.service';
|
||||||
import { LoveNowPlayingService } from './love-now-playing.service';
|
import { LoveNowPlayingService } from './love-now-playing.service';
|
||||||
|
import { RoksNowPlayingService } from './roks-now-playing.service';
|
||||||
import { ChartsModule } from '../charts/charts.module';
|
import { ChartsModule } from '../charts/charts.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -20,6 +21,7 @@ import { ChartsModule } from '../charts/charts.module';
|
|||||||
EmgNowPlayingService,
|
EmgNowPlayingService,
|
||||||
DfmNowPlayingService,
|
DfmNowPlayingService,
|
||||||
LoveNowPlayingService,
|
LoveNowPlayingService,
|
||||||
|
RoksNowPlayingService,
|
||||||
],
|
],
|
||||||
exports: [NowPlayingService],
|
exports: [NowPlayingService],
|
||||||
})
|
})
|
||||||
|
|||||||
97
src/now-playing/roks-now-playing.service.ts
Normal file
97
src/now-playing/roks-now-playing.service.ts
Normal file
@@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user