feat(now-playing): Love Radio через api.loveradio.ru (ICY шлёт мусор onlinestop56k)

ICY-потоки Love Radio отдают 'onlinestop56k' вместо трека. Берём текущий трек из
их API (player/history/list?musicStreamId=N&limit=1, data[0]). Статичный маппинг
наших станций -> musicStreamId. ICY-поллер исключает genre='Love Radio'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-03 18:19:12 +03:00
parent 338f189f33
commit fa7742d06e
3 changed files with 92 additions and 1 deletions

View File

@@ -29,7 +29,7 @@ export class IcyNowPlayingService {
const where = { const where = {
recordStationId: null, recordStationId: null,
isOnline: true, isOnline: true,
genre: { not: 'DFM' }, genre: { notIn: ['DFM', 'Love Radio'] },
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 });

View File

@@ -0,0 +1,89 @@
import { Injectable, Logger } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
import { NowPlayingService } from './now-playing.service';
interface LoveHistoryItem {
artistTitle?: string | null;
songTitle?: string | null;
}
/**
* Now-playing для Love Radio. В ICY-потоках вместо трека приходит «onlinestop56k»
* (имя мута), поэтому берём текущий трек из их API:
* api.loveradio.ru/api/v1/love-radio/player/history/list?musicStreamId={id}&limit=1
* (data[0] = последний/текущий). Обложек API не даёт — их подставит наше обогащение.
*/
@Injectable()
export class LoveNowPlayingService {
private readonly logger = new Logger(LoveNowPlayingService.name);
private readonly headers = {
'User-Agent': 'Mozilla/5.0',
Origin: 'https://www.loveradio.ru',
Referer: 'https://www.loveradio.ru/',
};
// Имя нашей станции -> musicStreamId в API Love Radio
private readonly streamId: Record<string, number> = {
'Love Radio': 28,
'Love RnB': 2,
'Love Top40': 3,
'Love Dance': 4,
'Love Chill': 5,
'Love Gold': 6,
'Love Russian': 7,
'Love KPOP': 10,
'Love Power': 11,
'Love Summer': 1,
};
constructor(
private readonly prisma: PrismaService,
private readonly nowPlayingService: NowPlayingService,
) {}
@Interval(30000)
async pollLoveNowPlaying() {
const stations = await this.prisma.station.findMany({
where: { genre: 'Love Radio' },
});
if (stations.length === 0) return;
let updated = 0;
await Promise.allSettled(
stations.map(async (station) => {
const id = this.streamId[station.name];
if (!id) return;
const url =
`https://api.loveradio.ru/api/v1/love-radio/player/history/list` +
`?musicStreamId=${id}&limit=1`;
const res = await fetch(url, { headers: this.headers });
if (!res.ok) return;
const json = (await res.json()) as { data?: LoveHistoryItem[] };
const cur = json.data?.[0];
const artist = (cur?.artistTitle ?? '').trim();
const song = (cur?.songTitle ?? '').trim();
if (!artist || !song) return;
await this.nowPlayingService.ingest({
stationDbId: station.id,
stationNumericId: station.stationId,
artist,
song,
coverUrl: null,
});
if (!station.isOnline) {
await this.prisma.station.update({
where: { id: station.id },
data: { isOnline: true },
});
}
updated++;
}),
);
this.logger.log(`Love poll: ${updated}/${stations.length} обновлено`);
}
}

View File

@@ -6,6 +6,7 @@ import { RecordStationSyncService } from './record-station-sync.service';
import { IcyNowPlayingService } from './icy-now-playing.service'; 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 { ChartsModule } from '../charts/charts.module'; import { ChartsModule } from '../charts/charts.module';
@Module({ @Module({
@@ -18,6 +19,7 @@ import { ChartsModule } from '../charts/charts.module';
IcyNowPlayingService, IcyNowPlayingService,
EmgNowPlayingService, EmgNowPlayingService,
DfmNowPlayingService, DfmNowPlayingService,
LoveNowPlayingService,
], ],
exports: [NowPlayingService], exports: [NowPlayingService],
}) })