feat(now-playing): DFM и др. ICY-станции — обложки + чарты + ротация
ICY-станции (DFM и пр.) теперь полноценно «как Record»: - ICY-поллер вызывает recordPlay → треки идут в чарты и обогащаются Discogs, откуда берётся обложка (раньше now_playing писался напрямую, мимо чартов) - обложка now-playing: если источник не дал (ICY всегда null) — подставляем обложку обогащённого трека из нашей БД по normKey (NowPlayingService.resolveCover) - ротация курсора по всем станциям (окно 70) вместо первых 50 по кругу — раньше 363 из 413 станций не опрашивались - общий NowPlayingService.ingest для Record и ICY (дедуп логики) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,42 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { NowPlayingGateway } from './now-playing.gateway';
|
||||
import { NowPlayingService } from './now-playing.service';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
|
||||
/**
|
||||
* Сбор now-playing для не-Record станций (DFM и др.) через ICY-метаданные потока.
|
||||
* Станций много (сотни), поэтому за один тик опрашиваем окно и сдвигаем курсор —
|
||||
* за несколько минут проходим все по кругу. Обложку и зачёт в чарты/обогащение
|
||||
* берёт на себя NowPlayingService.ingest (обложка подтянется из нашей БД).
|
||||
*/
|
||||
@Injectable()
|
||||
export class IcyNowPlayingService {
|
||||
private readonly logger = new Logger(IcyNowPlayingService.name);
|
||||
private cursor = 0;
|
||||
private readonly windowSize = 70;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly gateway: NowPlayingGateway,
|
||||
private readonly nowPlayingService: NowPlayingService,
|
||||
) {}
|
||||
|
||||
@Interval(60000)
|
||||
async pollIcyNowPlaying() {
|
||||
this.logger.log('Starting ICY now playing poll...');
|
||||
const where = { recordStationId: null, isOnline: true };
|
||||
const total = await this.prisma.station.count({ where });
|
||||
if (total === 0) return;
|
||||
if (this.cursor >= total) this.cursor = 0;
|
||||
const offset = this.cursor;
|
||||
|
||||
const stations = await this.prisma.station.findMany({
|
||||
where: { recordStationId: null, isOnline: true },
|
||||
take: 50,
|
||||
where,
|
||||
orderBy: { stationId: 'asc' },
|
||||
skip: offset,
|
||||
take: this.windowSize,
|
||||
});
|
||||
this.cursor += this.windowSize;
|
||||
|
||||
let successCount = 0;
|
||||
|
||||
@@ -29,47 +45,26 @@ export class IcyNowPlayingService {
|
||||
const results = await Promise.allSettled(
|
||||
batch.map(async (station) => {
|
||||
const track = await this.parseIcyMetadata(station.streamUrl);
|
||||
if (!track) return null;
|
||||
if (!track || !track.artist || !track.song) return null;
|
||||
|
||||
const updated = await this.prisma.nowPlaying.upsert({
|
||||
where: { stationId: station.id },
|
||||
create: {
|
||||
stationId: station.id,
|
||||
song: track.song,
|
||||
artist: track.artist,
|
||||
coverUrl: null,
|
||||
},
|
||||
update: {
|
||||
song: track.song,
|
||||
artist: track.artist,
|
||||
coverUrl: null,
|
||||
},
|
||||
});
|
||||
|
||||
this.gateway.broadcastNowPlaying(station.stationId.toString(), {
|
||||
song: track.song,
|
||||
await this.nowPlayingService.ingest({
|
||||
stationDbId: station.id,
|
||||
stationNumericId: station.stationId,
|
||||
artist: track.artist,
|
||||
song: track.song,
|
||||
coverUrl: null,
|
||||
updatedAt: updated.updatedAt,
|
||||
});
|
||||
return track;
|
||||
}),
|
||||
);
|
||||
|
||||
for (let j = 0; j < results.length; j++) {
|
||||
const result = results[j];
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
successCount++;
|
||||
} else if (result.status === 'rejected') {
|
||||
this.logger.warn(
|
||||
`ICY failed for ${batch[j].name}: ${result.reason?.message || result.reason}`,
|
||||
);
|
||||
}
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value) successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`ICY poll complete: ${successCount}/${stations.length} stations updated`,
|
||||
`ICY poll: ${successCount}/${stations.length} updated (offset ${offset}/${total})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user