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, '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user