feat(now-playing): DFM/Крутой Медиа через dfm.ru/api/n/current
Сабканалы DFM (Skrillex, Daft Punk, K-Pop, Игромания и др.) не отдают ICY-метаданные. Единый веб-API dfm.ru/api/n/current даёт текущий трек + WebP-обложку по всем ~147 каналам (ключ slug). DfmNowPlayingService матчит наши DFM-станции по нормализованному имени (+ числовой префикс, + алиасы для годов/Игромании/Pioneer). ICY-поллер исключает genre=DFM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
121
src/now-playing/dfm-now-playing.service.ts
Normal file
121
src/now-playing/dfm-now-playing.service.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Interval } from '@nestjs/schedule';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { NowPlayingService } from './now-playing.service';
|
||||||
|
|
||||||
|
interface DfmCurrent {
|
||||||
|
artist?: string;
|
||||||
|
title?: string;
|
||||||
|
cover?: string;
|
||||||
|
genres?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now-playing для станций Крутой Медиа (DFM и все его сабканалы — Skrillex,
|
||||||
|
* Daft Punk, K-Pop, Игромания и т.д.). Единый веб-API dfm.ru/api/n/current
|
||||||
|
* отдаёт текущий трек + WebP-обложку по всем ~147 каналам (ключ — slug).
|
||||||
|
* Сопоставляем наши станции (группа DFM) по нормализованному имени.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DfmNowPlayingService {
|
||||||
|
private readonly logger = new Logger(DfmNowPlayingService.name);
|
||||||
|
private readonly headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0',
|
||||||
|
Referer: 'https://dfm.ru/',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Стойкие случаи: нормализованное имя нашей станции -> slug в API
|
||||||
|
private readonly alias: Record<string, string> = {
|
||||||
|
'dance-gold-00-s': 'dance-gold-2000s',
|
||||||
|
'dance-gold-10-s': 'dance-gold-2010s',
|
||||||
|
'dance-gold-20-s': 'dance-gold-2020s',
|
||||||
|
'dance-gold-90-s': 'dance-gold-1990s',
|
||||||
|
'pop-gold-00-s': 'pop-gold-2000s',
|
||||||
|
'pop-gold-10-s': 'pop-gold-2010s',
|
||||||
|
'pop-gold-20-s': 'pop-gold2020s',
|
||||||
|
'pop-gold-90-s': 'pop-gold-1990s',
|
||||||
|
'festival-gold': 'festivals-gold',
|
||||||
|
pioneer: '59-dfm-pioneer',
|
||||||
|
игромания: '61-igromaniq',
|
||||||
|
'vocal-trance': 'trance',
|
||||||
|
'disco-90th': 'diskach-90h',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly nowPlayingService: NowPlayingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Interval(30000)
|
||||||
|
async pollDfmNowPlaying() {
|
||||||
|
const stations = await this.prisma.station.findMany({
|
||||||
|
where: { genre: 'DFM' },
|
||||||
|
});
|
||||||
|
if (stations.length === 0) return;
|
||||||
|
|
||||||
|
const res = await fetch('https://dfm.ru/api/n/current', {
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
this.logger.warn(`DFM api/n/current вернул ${res.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = (await res.json()) as {
|
||||||
|
result?: { data?: Record<string, { current?: DfmCurrent }> };
|
||||||
|
};
|
||||||
|
const data = json.result?.data;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
// Индекс: slug и его варианты (без дефисов, без числового префикса) -> current
|
||||||
|
const idx = new Map<string, DfmCurrent>();
|
||||||
|
for (const slug of Object.keys(data)) {
|
||||||
|
const cur = data[slug].current;
|
||||||
|
if (!cur?.artist || !cur?.title) continue;
|
||||||
|
const base = slug.replace(/^\d+-/, '');
|
||||||
|
for (const key of [slug, slug.replace(/-/g, ''), base, base.replace(/-/g, '')]) {
|
||||||
|
if (!idx.has(key)) idx.set(key, cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = 0;
|
||||||
|
for (const station of stations) {
|
||||||
|
const n = this.norm(station.name);
|
||||||
|
const aliasSlug = this.alias[n];
|
||||||
|
const cur =
|
||||||
|
idx.get(n) ??
|
||||||
|
idx.get(n.replace(/-/g, '')) ??
|
||||||
|
(aliasSlug ? data[aliasSlug]?.current : undefined);
|
||||||
|
if (!cur?.artist || !cur?.title) continue;
|
||||||
|
|
||||||
|
const cover = cur.cover
|
||||||
|
? cur.cover.startsWith('http')
|
||||||
|
? cur.cover
|
||||||
|
: `https://dfm.ru${cur.cover}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await this.nowPlayingService.ingest({
|
||||||
|
stationDbId: station.id,
|
||||||
|
stationNumericId: station.stationId,
|
||||||
|
artist: cur.artist.trim(),
|
||||||
|
song: cur.title.trim(),
|
||||||
|
coverUrl: cover,
|
||||||
|
});
|
||||||
|
if (!station.isOnline) {
|
||||||
|
await this.prisma.station.update({
|
||||||
|
where: { id: station.id },
|
||||||
|
data: { isOnline: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`DFM poll: ${updated}/${stations.length} обновлено`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private norm(s: string): string {
|
||||||
|
return s
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9а-я]+/gi, '-')
|
||||||
|
.replace(/^-|-$/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,11 +24,12 @@ export class IcyNowPlayingService {
|
|||||||
|
|
||||||
@Interval(60000)
|
@Interval(60000)
|
||||||
async pollIcyNowPlaying() {
|
async pollIcyNowPlaying() {
|
||||||
// emgsound (ЕМГ) обрабатывает EmgNowPlayingService через meta-API; их HLS-потоки
|
// ЕМГ (emgsound) и DFM/Крутой (genre=DFM) обрабатываются отдельными сервисами
|
||||||
// ICY-метаданных не отдают — исключаем, чтобы не тратить слоты впустую.
|
// через их API — исключаем из ICY, чтобы не тратить слоты впустую.
|
||||||
const where = {
|
const where = {
|
||||||
recordStationId: null,
|
recordStationId: null,
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
|
genre: { not: 'DFM' },
|
||||||
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 });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NowPlayingService } from './now-playing.service';
|
|||||||
import { RecordStationSyncService } from './record-station-sync.service';
|
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 { ChartsModule } from '../charts/charts.module';
|
import { ChartsModule } from '../charts/charts.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -16,6 +17,7 @@ import { ChartsModule } from '../charts/charts.module';
|
|||||||
RecordStationSyncService,
|
RecordStationSyncService,
|
||||||
IcyNowPlayingService,
|
IcyNowPlayingService,
|
||||||
EmgNowPlayingService,
|
EmgNowPlayingService,
|
||||||
|
DfmNowPlayingService,
|
||||||
],
|
],
|
||||||
exports: [NowPlayingService],
|
exports: [NowPlayingService],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user