diff --git a/src/now-playing/icy-now-playing.service.ts b/src/now-playing/icy-now-playing.service.ts index 5533735..4736d79 100644 --- a/src/now-playing/icy-now-playing.service.ts +++ b/src/now-playing/icy-now-playing.service.ts @@ -37,6 +37,7 @@ export class IcyNowPlayingService { 'Radio Monte Carlo', 'Radio ROKS', 'Unistar', + 'Zaicev FM', ], }, NOT: { streamUrl: { contains: 'emgsound.ru' } }, diff --git a/src/now-playing/now-playing.module.ts b/src/now-playing/now-playing.module.ts index 47a918e..dacddb4 100644 --- a/src/now-playing/now-playing.module.ts +++ b/src/now-playing/now-playing.module.ts @@ -9,6 +9,7 @@ import { DfmNowPlayingService } from './dfm-now-playing.service'; 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 { ChartsModule } from '../charts/charts.module'; @Module({ @@ -24,6 +25,7 @@ import { ChartsModule } from '../charts/charts.module'; LoveNowPlayingService, RoksNowPlayingService, UnistarNowPlayingService, + ZaycevNowPlayingService, ], exports: [NowPlayingService], }) diff --git a/src/now-playing/zaycev-now-playing.service.ts b/src/now-playing/zaycev-now-playing.service.ts new file mode 100644 index 0000000..278a401 --- /dev/null +++ b/src/now-playing/zaycev-now-playing.service.ts @@ -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; + } +}