import { Injectable, Logger } from '@nestjs/common'; import { Interval } from '@nestjs/schedule'; import { PrismaService } from '../prisma/prisma.service'; import { NowPlayingService } from './now-playing.service'; /** * Now-playing для «Русская Волна» (сеть Волна / amgradio.ru, ~27 каналов). Потоки * mp3.amgradio.ru НЕ отдают ICY. Единый JSON со всеми каналами: * `https://info.volna.top/radio.json` → поля `{prefix}_title` = «АРТИСТ - ПЕСНЯ». * Маунт нашего потока (RusRock128, ChillaFM128…) приводим к префиксу volna * (rusrock, chilla — с fallback на отброс «fm»). Обложек у них нет (covers 404) → * подтянет обогащение (iTunes/Deezer). */ @Injectable() export class VolnaNowPlayingService { private readonly logger = new Logger(VolnaNowPlayingService.name); private readonly headers = { 'User-Agent': 'Mozilla/5.0' }; constructor( private readonly prisma: PrismaService, private readonly nowPlayingService: NowPlayingService, ) {} @Interval(30000) async pollVolnaNowPlaying() { const stations = await this.prisma.station.findMany({ where: { streamUrl: { contains: 'amgradio.ru' } }, }); if (stations.length === 0) return; let data: Record | null = null; try { const res = await fetch('https://info.volna.top/radio.json', { headers: this.headers, }); if (res.ok) data = (await res.json()) as Record; } catch { data = null; } if (!data) return; const prefixes = new Set( Object.keys(data) .filter((k) => k.endsWith('_title')) .map((k) => k.slice(0, -'_title'.length)), ); let updated = 0; await Promise.allSettled( stations.map(async (station) => { const prefix = this.resolvePrefix(station.streamUrl, prefixes); if (!prefix) return; const raw = data[`${prefix}_title`]; const parsed = this.parseTitle(typeof raw === 'string' ? raw : undefined); if (!parsed) return; await this.nowPlayingService.ingest({ stationDbId: station.id, stationNumericId: station.stationId, artist: parsed.artist, song: parsed.song, coverUrl: null, }); if (!station.isOnline) { await this.prisma.station.update({ where: { id: station.id }, data: { isOnline: true }, }); } updated++; }), ); this.logger.log(`Volna poll: ${updated}/${stations.length} обновлено`); } // mp3.amgradio.ru/RusRock128 → rusrock ; ChillaFM128 → chilla (fallback -fm) private resolvePrefix(streamUrl: string, prefixes: Set): string | null { const m = streamUrl.match(/amgradio\.ru\/([A-Za-z0-9_]+)/i); if (!m) return null; const norm = m[1].replace(/\d+$/, '').toLowerCase(); if (prefixes.has(norm)) return norm; if (norm.endsWith('fm') && prefixes.has(norm.slice(0, -2))) { return norm.slice(0, -2); } return null; } private parseTitle(title?: string): { artist: string; song: string } | null { if (!title) return null; const t = title.trim(); if (!t || /listen radio|^https?:/i.test(t)) return null; const idx = t.indexOf(' - '); if (idx < 0) return null; const artist = t.slice(0, idx).trim(); const song = t.slice(idx + 3).trim(); if (!artist || !song) return null; return { artist, song }; } }