Files
radiola-backend/src/now-playing/volna-now-playing.service.ts
nk c4c475544a feat(now-playing): Радио Ваня + Русская Волна; Питер объединён в SpbRadio
Радио Ваня (20 каналов) — тот же движок/API, что Питер ФМ (один разработчик):
объединил в SpbRadioNowPlayingService (NETWORKS=[piterfm, radiovanya]), матч
станции по МАУНТУ из поля link (у Вани slug≠маунт). Обложки iTunes.
Русская Волна (~27, amgradio.ru, ICY нет) — VolnaNowPlayingService: единый
info.volna.top/radio.json, поля {prefix}_title, маунт→префикс (RusRock128→rusrock,
ChillaFM128→chilla). Обложки через обогащение. Оба жанра исключены из ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 09:18:23 +03:00

100 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, unknown> | 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<string, unknown>;
} 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>): 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 };
}
}