Все 21 канал Монте-Карло — сеть Крутой Медиа (dfm.ru/api/n/current). Добавил genre='Radio Monte Carlo' в DfmNowPlayingService, матчинг по слагу из маута потока (basename без битрейта: blues96.aacp -> blues), исключил из ICY-поллера. Чинит 5 каналов, залипших на 'Дух — Тишина' (Blues, Chill Lounge, Italiano, Meditation, Summertime).
150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
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',
|
||
// MAXIMUM (тот же Крутой Медиа, тот же /api/n/current)
|
||
britpop: '130-maxbritpop',
|
||
covers: '129-maxcover',
|
||
'heavy-80-s': '131-max80',
|
||
'heavy-monday': '141-heavymonday',
|
||
'maximum-90th': '145-maximum90',
|
||
millenium: '140-millenium',
|
||
'new-russians': '125-maxnewrussians',
|
||
punk: '132-maxpunk',
|
||
rhcp: '123-maxrhcp',
|
||
'rock-hits': '144-rockhits',
|
||
rugby: '139-rugby',
|
||
'russian-rock': '90-russkijrok',
|
||
soft: '127-maxsoft',
|
||
};
|
||
|
||
constructor(
|
||
private readonly prisma: PrismaService,
|
||
private readonly nowPlayingService: NowPlayingService,
|
||
) {}
|
||
|
||
@Interval(30000)
|
||
async pollDfmNowPlaying() {
|
||
// DFM, MAXIMUM и Радио Монте-Карло — все сети Крутой Медиа, общий API
|
||
// dfm.ru/api/n/current
|
||
const stations = await this.prisma.station.findMany({
|
||
where: { genre: { in: ['DFM', 'MAXIMUM', 'Radio Monte Carlo'] } },
|
||
});
|
||
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];
|
||
// Слаг из маута потока (basename без битрейта) — основной ключ для
|
||
// Монте-Карло (имя станции не совпадает с API-слагом, а маут совпадает:
|
||
// `mccovers96.aacp` → `mccovers`, `blues96.aacp` → `blues`).
|
||
const mount = this.mountSlug(station.streamUrl);
|
||
const cur =
|
||
idx.get(n) ??
|
||
idx.get(n.replace(/-/g, '')) ??
|
||
(mount ? idx.get(mount) : undefined) ??
|
||
(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(`Krutoy poll: ${updated}/${stations.length} обновлено`);
|
||
}
|
||
|
||
private norm(s: string): string {
|
||
return s
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9а-я]+/gi, '-')
|
||
.replace(/^-|-$/g, '');
|
||
}
|
||
|
||
// Слаг из маута потока: basename пути без расширения и хвостового битрейта.
|
||
// `http://mc-blues.hostingradio.ru/blues96.aacp` → `blues`.
|
||
private mountSlug(streamUrl: string): string | null {
|
||
const m = streamUrl.match(/\/([a-z0-9_-]+?)\d*\.(?:aacp|aac|mp3|m3u8)/i);
|
||
return m ? m[1].toLowerCase() : null;
|
||
}
|
||
}
|