From cc30422d8d5f55e17d34a5bfd61f8e7f87a9114c Mon Sep 17 00:00:00 2001 From: nk Date: Sat, 6 Jun 2026 09:29:41 +0300 Subject: [PATCH] =?UTF-8?q?feat(now-playing):=20101.ru=20(Comedy=20Radio?= =?UTF-8?q?=20+=20=D0=A0=D0=B0=D0=B4=D0=B8=D0=BE=20Energy)=20=D0=B8=20Radi?= =?UTF-8?q?o=207=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20EMG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 101.ru (Comedy, NRJ/Energy, ~15 каналов): id канала = последний сегмент потока pub*.101.ru/.../{id}; трек GET 101.ru/api/channel/getTrackOnAir/{id}/?idcity=1 → result.short {titleExecutorFull, titleTrack, cover.coverOriginal}; обложка cdn0.101.ru. Radio 7 — это ЕМГ на старых мейнах radio7.hostingradio.ru: расширил EmgNowPlayingService (slug radio7128→radio7, radio7_love64→radio7-love). Три жанра исключены из ICY-поллера. Co-Authored-By: Claude Opus 4.8 --- src/now-playing/emg-now-playing.service.ts | 21 ++++- src/now-playing/icy-now-playing.service.ts | 3 + src/now-playing/now-playing.module.ts | 2 + .../radio101-now-playing.service.ts | 84 +++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/now-playing/radio101-now-playing.service.ts diff --git a/src/now-playing/emg-now-playing.service.ts b/src/now-playing/emg-now-playing.service.ts index 1a44fdc..4f2975e 100644 --- a/src/now-playing/emg-now-playing.service.ts +++ b/src/now-playing/emg-now-playing.service.ts @@ -35,8 +35,14 @@ export class EmgNowPlayingService { @Interval(30000) async pollEmgNowPlaying() { // Не фильтруем по isOnline: health-check ошибочно метит HLS-потоки offline. + // Radio 7 — тоже ЕМГ, но на старых мейнах radio7.hostingradio.ru (slug в meta). const stations = await this.prisma.station.findMany({ - where: { streamUrl: { contains: 'emgsound.ru' } }, + where: { + OR: [ + { streamUrl: { contains: 'emgsound.ru' } }, + { streamUrl: { contains: 'radio7.hostingradio.ru' } }, + ], + }, }); if (stations.length === 0) return; @@ -93,9 +99,18 @@ export class EmgNowPlayingService { } // hls-01-europaplus-kpop.emgsound.ru → europaplus-kpop + // radio7.hostingradio.ru:8040/radio7_love64.mp3 → radio7-love (strip bitrate, _→-) private extractSlug(streamUrl: string): string | null { - const m = streamUrl.match(/hls-\d+-([a-z0-9-]+)\.emgsound\.ru/i); - return m ? m[1].toLowerCase() : null; + const emg = streamUrl.match(/hls-\d+-([a-z0-9-]+)\.emgsound\.ru/i); + if (emg) return emg[1].toLowerCase(); + const r7 = streamUrl.match(/radio7\.hostingradio\.ru[:0-9]*\/([a-z0-9_]+)/i); + if (r7) { + return r7[1] + .toLowerCase() + .replace(/\d+$/, '') + .replace(/_/g, '-'); + } + return null; } // Дата и окно времени по Москве (контейнер может быть в UTC) diff --git a/src/now-playing/icy-now-playing.service.ts b/src/now-playing/icy-now-playing.service.ts index b861378..87f22fc 100644 --- a/src/now-playing/icy-now-playing.service.ts +++ b/src/now-playing/icy-now-playing.service.ts @@ -44,6 +44,9 @@ export class IcyNowPlayingService { 'Орфей', 'Радио Ваня', 'Русская Волна', + 'Comedy Radio', + 'Радио Energy', + 'Radio 7', ], }, NOT: { streamUrl: { contains: 'emgsound.ru' } }, diff --git a/src/now-playing/now-playing.module.ts b/src/now-playing/now-playing.module.ts index 53ed99c..2e519cb 100644 --- a/src/now-playing/now-playing.module.ts +++ b/src/now-playing/now-playing.module.ts @@ -14,6 +14,7 @@ import { GooseNowPlayingService } from './goose-now-playing.service'; import { NovoeByNowPlayingService } from './novoeby-now-playing.service'; import { SpbRadioNowPlayingService } from './spb-radio-now-playing.service'; import { VolnaNowPlayingService } from './volna-now-playing.service'; +import { Radio101NowPlayingService } from './radio101-now-playing.service'; import { OrpheusNowPlayingService } from './orpheus-now-playing.service'; import { ChartsModule } from '../charts/charts.module'; @@ -35,6 +36,7 @@ import { ChartsModule } from '../charts/charts.module'; NovoeByNowPlayingService, SpbRadioNowPlayingService, VolnaNowPlayingService, + Radio101NowPlayingService, OrpheusNowPlayingService, ], exports: [NowPlayingService], diff --git a/src/now-playing/radio101-now-playing.service.ts b/src/now-playing/radio101-now-playing.service.ts new file mode 100644 index 0000000..c80bce3 --- /dev/null +++ b/src/now-playing/radio101-now-playing.service.ts @@ -0,0 +1,84 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Interval } from '@nestjs/schedule'; +import { PrismaService } from '../prisma/prisma.service'; +import { NowPlayingService } from './now-playing.service'; + +interface Radio101Resp { + result?: { + short?: { + titleTrack?: string; + titleExecutorFull?: string; + cover?: { coverOriginal?: string }; + }; + }; +} + +/** + * Now-playing для станций на платформе 101.ru (Comedy Radio, Радио Energy/NRJ). + * Потоки pub*.101.ru/stream/.../{channelId} — id канала = последний сегмент. + * Трек: `https://101.ru/api/channel/getTrackOnAir/{id}/?dataFormat=json&idcity=1` + * → result.short {titleExecutorFull, titleTrack, cover.coverOriginal}. Обложка — + * cdn0.101.ru + coverOriginal. (Comedy: «комик - реприза» — это их «в эфире».) + */ +@Injectable() +export class Radio101NowPlayingService { + private readonly logger = new Logger(Radio101NowPlayingService.name); + private readonly coverBase = 'https://cdn0.101.ru'; + private readonly headers = { 'User-Agent': 'Mozilla/5.0' }; + + constructor( + private readonly prisma: PrismaService, + private readonly nowPlayingService: NowPlayingService, + ) {} + + @Interval(30000) + async pollRadio101NowPlaying() { + const stations = await this.prisma.station.findMany({ + where: { streamUrl: { contains: '.101.ru' } }, + }); + if (stations.length === 0) return; + + let updated = 0; + await Promise.allSettled( + stations.map(async (station) => { + const id = this.extractChannelId(station.streamUrl); + if (!id) return; + + const res = await fetch( + `https://101.ru/api/channel/getTrackOnAir/${id}/?dataFormat=json&idcity=1`, + { headers: this.headers, redirect: 'follow' }, + ); + if (!res.ok) return; + const short = ((await res.json()) as Radio101Resp).result?.short; + const artist = (short?.titleExecutorFull ?? '').trim(); + const song = (short?.titleTrack ?? '').trim(); + if (!artist || !song) return; + + const coverPath = short?.cover?.coverOriginal; + const coverUrl = coverPath ? this.coverBase + coverPath : 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(`Radio101 poll: ${updated}/${stations.length} обновлено`); + } + + // https://pub0302.101.ru:8000/stream/pro/aac/64/446 → 446 + private extractChannelId(streamUrl: string): string | null { + const m = streamUrl.match(/\/(\d+)\/?$/); + return m ? m[1] : null; + } +}