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 = { '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 }; }; const data = json.result?.data; if (!data) return; // Индекс: slug и его варианты (без дефисов, без числового префикса) -> current const idx = new Map(); 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; } }