feat(now-playing): ГУСЬ (radiogoose) — now-playing + обложки, починка потоков

Сеть ГУСЬ (16 каналов) на AzuraCast (radiogoose.ru). Потоки в каталоге были
многострочными (url1\nurl2) → клиент рвал воспроизведение, health-check метил
offline, станции скрывались. Почищены на канонический https://radiogoose.ru/listen/{slug}/play.
GooseNowPlayingService (@Interval 30с): AzuraCast API /api/nowplaying/{slug} →
now_playing.song {artist,title,art}, ingest + isOnline=true. Исключён из ICY-поллера.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-05 20:22:28 +03:00
parent fd26e4df57
commit cb0e401854
4 changed files with 108 additions and 16 deletions

View File

@@ -0,0 +1,89 @@
import { Injectable, Logger } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
import { NowPlayingService } from './now-playing.service';
// Ответ AzuraCast https://radiogoose.ru/api/nowplaying/{slug}
interface AzuraNowPlaying {
is_online?: boolean;
now_playing?: {
song?: {
artist?: string;
title?: string;
art?: string;
};
};
}
/**
* Now-playing для сети ГУСЬ (radiogoose.ru) — это AzuraCast, у него штатный
* публичный API текущего трека с обложкой:
* GET https://radiogoose.ru/api/nowplaying/{slug} → now_playing.song {artist,title,art}.
* slug = сегмент потока /listen/{slug}/play. is_online из ответа поправляет
* ошибочный offline-флаг (health-check ранее спотыкался о многострочный URL).
*/
@Injectable()
export class GooseNowPlayingService {
private readonly logger = new Logger(GooseNowPlayingService.name);
private readonly headers = { 'User-Agent': 'Mozilla/5.0' };
constructor(
private readonly prisma: PrismaService,
private readonly nowPlayingService: NowPlayingService,
) {}
@Interval(30000)
async pollGooseNowPlaying() {
const stations = await this.prisma.station.findMany({
where: { streamUrl: { contains: 'radiogoose.ru' } },
});
if (stations.length === 0) return;
let updated = 0;
await Promise.allSettled(
stations.map(async (station) => {
const slug = this.extractSlug(station.streamUrl);
if (!slug) return;
const res = await fetch(
`https://radiogoose.ru/api/nowplaying/${slug}`,
{ headers: this.headers },
);
if (!res.ok) return;
const data = (await res.json()) as AzuraNowPlaying;
if (data.is_online === false) return;
const song = data.now_playing?.song;
const artist = (song?.artist ?? '').trim();
const title = (song?.title ?? '').trim();
if (!artist || !title) return;
await this.nowPlayingService.ingest({
stationDbId: station.id,
stationNumericId: station.stationId,
artist,
song: title,
coverUrl: song?.art?.trim() || null,
});
if (!station.isOnline) {
await this.prisma.station.update({
where: { id: station.id },
data: { isOnline: true },
});
}
updated++;
}),
);
this.logger.log(`Goose poll: ${updated}/${stations.length} обновлено`);
}
// https://radiogoose.ru/listen/bigroom/play → bigroom
private extractSlug(streamUrl: string): string | null {
const m = streamUrl.match(/\/listen\/([a-z0-9]+)\/play/i);
return m ? m[1].toLowerCase() : null;
}
}

View File

@@ -38,6 +38,7 @@ export class IcyNowPlayingService {
'Radio ROKS',
'Unistar',
'Zaicev FM',
'Гусь',
],
},
NOT: { streamUrl: { contains: 'emgsound.ru' } },

View File

@@ -10,6 +10,7 @@ import { LoveNowPlayingService } from './love-now-playing.service';
import { RoksNowPlayingService } from './roks-now-playing.service';
import { UnistarNowPlayingService } from './unistar-now-playing.service';
import { ZaycevNowPlayingService } from './zaycev-now-playing.service';
import { GooseNowPlayingService } from './goose-now-playing.service';
import { ChartsModule } from '../charts/charts.module';
@Module({
@@ -26,6 +27,7 @@ import { ChartsModule } from '../charts/charts.module';
RoksNowPlayingService,
UnistarNowPlayingService,
ZaycevNowPlayingService,
GooseNowPlayingService,
],
exports: [NowPlayingService],
})