feat(now-playing): now-playing + обложки для Зайцев ФМ
19 каналов Зайцев ФМ (MP3 abs.zaycev.fm) не отдают ICY → трека не было. ZaycevNowPlayingService (@Interval 30с): тянет текущий трек из API сайта GET https://www.zaycev.fm/api/v1/recent?channel={slug}&limit=1 (slug = буквенная часть имени потока, pop256k→pop), берёт artist/title и готовую обложку (radio2.zaycev.fm/artistimages), фильтр is_music. Зайцев исключён из ICY-поллера. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ export class IcyNowPlayingService {
|
|||||||
'Radio Monte Carlo',
|
'Radio Monte Carlo',
|
||||||
'Radio ROKS',
|
'Radio ROKS',
|
||||||
'Unistar',
|
'Unistar',
|
||||||
|
'Zaicev FM',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
NOT: { streamUrl: { contains: 'emgsound.ru' } },
|
NOT: { streamUrl: { contains: 'emgsound.ru' } },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DfmNowPlayingService } from './dfm-now-playing.service';
|
|||||||
import { LoveNowPlayingService } from './love-now-playing.service';
|
import { LoveNowPlayingService } from './love-now-playing.service';
|
||||||
import { RoksNowPlayingService } from './roks-now-playing.service';
|
import { RoksNowPlayingService } from './roks-now-playing.service';
|
||||||
import { UnistarNowPlayingService } from './unistar-now-playing.service';
|
import { UnistarNowPlayingService } from './unistar-now-playing.service';
|
||||||
|
import { ZaycevNowPlayingService } from './zaycev-now-playing.service';
|
||||||
import { ChartsModule } from '../charts/charts.module';
|
import { ChartsModule } from '../charts/charts.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -24,6 +25,7 @@ import { ChartsModule } from '../charts/charts.module';
|
|||||||
LoveNowPlayingService,
|
LoveNowPlayingService,
|
||||||
RoksNowPlayingService,
|
RoksNowPlayingService,
|
||||||
UnistarNowPlayingService,
|
UnistarNowPlayingService,
|
||||||
|
ZaycevNowPlayingService,
|
||||||
],
|
],
|
||||||
exports: [NowPlayingService],
|
exports: [NowPlayingService],
|
||||||
})
|
})
|
||||||
|
|||||||
100
src/now-playing/zaycev-now-playing.service.ts
Normal file
100
src/now-playing/zaycev-now-playing.service.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Interval } from '@nestjs/schedule';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { NowPlayingService } from './now-playing.service';
|
||||||
|
|
||||||
|
// Ответ https://www.zaycev.fm/api/v1/recent?channel={slug}&limit=1
|
||||||
|
interface ZaycevRecentItem {
|
||||||
|
track?: {
|
||||||
|
artist?: string;
|
||||||
|
title?: string;
|
||||||
|
is_music?: boolean;
|
||||||
|
img?: string;
|
||||||
|
images?: {
|
||||||
|
medium?: string;
|
||||||
|
large?: string;
|
||||||
|
extralarge?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
station_alias?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now-playing для каналов Зайцев ФМ. Их MP3-потоки (abs.zaycev.fm/{slug}256k)
|
||||||
|
* НЕ отдают ICY-метаданных, поэтому трек берём из API сайта:
|
||||||
|
* GET https://www.zaycev.fm/api/v1/recent?channel={slug}&limit=1 — массив, [0] =
|
||||||
|
* текущий трек с artist/title и готовыми обложками (radio2.zaycev.fm/artistimages).
|
||||||
|
* slug = буквенная часть имени потока (pop256k → pop), совпадает со station_alias.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ZaycevNowPlayingService {
|
||||||
|
private readonly logger = new Logger(ZaycevNowPlayingService.name);
|
||||||
|
private readonly headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0',
|
||||||
|
Referer: 'https://www.zaycev.fm/',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly nowPlayingService: NowPlayingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Interval(30000)
|
||||||
|
async pollZaycevNowPlaying() {
|
||||||
|
const stations = await this.prisma.station.findMany({
|
||||||
|
where: { streamUrl: { contains: 'abs.zaycev.fm' } },
|
||||||
|
});
|
||||||
|
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://www.zaycev.fm/api/v1/recent?channel=${slug}&limit=1`,
|
||||||
|
{ headers: this.headers },
|
||||||
|
);
|
||||||
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
const arr = (await res.json()) as ZaycevRecentItem[] | unknown;
|
||||||
|
const cur = Array.isArray(arr) ? arr[0] : null;
|
||||||
|
const t = cur?.track;
|
||||||
|
if (!t || t.is_music === false) return;
|
||||||
|
|
||||||
|
const artist = (t.artist ?? '').trim();
|
||||||
|
const song = (t.title ?? '').trim();
|
||||||
|
if (!artist || !song) return;
|
||||||
|
|
||||||
|
const coverUrl =
|
||||||
|
t.images?.large ?? t.images?.extralarge ?? t.images?.medium ?? t.img ?? null;
|
||||||
|
|
||||||
|
await this.nowPlayingService.ingest({
|
||||||
|
stationDbId: station.id,
|
||||||
|
stationNumericId: station.stationId,
|
||||||
|
artist,
|
||||||
|
song,
|
||||||
|
coverUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!station.isOnline) {
|
||||||
|
await this.prisma.station.update({
|
||||||
|
where: { id: station.id },
|
||||||
|
data: { isOnline: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updated++;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`Zaycev poll: ${updated}/${stations.length} обновлено`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://abs.zaycev.fm/pop256k → pop ; rurock256k → rurock
|
||||||
|
private extractSlug(streamUrl: string): string | null {
|
||||||
|
const m = streamUrl.match(/abs\.zaycev\.fm\/([a-z]+)\d/i);
|
||||||
|
return m ? m[1].toLowerCase() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user