From 4d9fd240743adf2655e5f416063f69cbf6fc7687 Mon Sep 17 00:00:00 2001 From: nk Date: Fri, 5 Jun 2026 19:37:17 +0300 Subject: [PATCH] =?UTF-8?q?feat(now-playing):=20now-playing=20+=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=B6=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20Unistar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8 HLS-каналов Unistar (Беларусь) не отдают ICY, поэтому трек брали неоткуда. Добавлен UnistarNowPlayingService (@Interval 30с): тянет текущий трек из их API https://api3.unistar.by/client/latest/{slug} (slug = сегмент /hls/{slug}/ потока), берёт Artist/Title и имя файла обложки (unistar.by/upload/music/photos/), ingest'ит. Только Type=Music. Unistar исключён из ICY-поллера. Co-Authored-By: Claude Opus 4.8 --- src/now-playing/icy-now-playing.service.ts | 1 + src/now-playing/now-playing.module.ts | 2 + .../unistar-now-playing.service.ts | 106 ++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/now-playing/unistar-now-playing.service.ts diff --git a/src/now-playing/icy-now-playing.service.ts b/src/now-playing/icy-now-playing.service.ts index 54b0749..5533735 100644 --- a/src/now-playing/icy-now-playing.service.ts +++ b/src/now-playing/icy-now-playing.service.ts @@ -36,6 +36,7 @@ export class IcyNowPlayingService { 'Love Radio', 'Radio Monte Carlo', 'Radio ROKS', + 'Unistar', ], }, NOT: { streamUrl: { contains: 'emgsound.ru' } }, diff --git a/src/now-playing/now-playing.module.ts b/src/now-playing/now-playing.module.ts index 21c2a04..47a918e 100644 --- a/src/now-playing/now-playing.module.ts +++ b/src/now-playing/now-playing.module.ts @@ -8,6 +8,7 @@ import { EmgNowPlayingService } from './emg-now-playing.service'; 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 { ChartsModule } from '../charts/charts.module'; @Module({ @@ -22,6 +23,7 @@ import { ChartsModule } from '../charts/charts.module'; DfmNowPlayingService, LoveNowPlayingService, RoksNowPlayingService, + UnistarNowPlayingService, ], exports: [NowPlayingService], }) diff --git a/src/now-playing/unistar-now-playing.service.ts b/src/now-playing/unistar-now-playing.service.ts new file mode 100644 index 0000000..87bd6df --- /dev/null +++ b/src/now-playing/unistar-now-playing.service.ts @@ -0,0 +1,106 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Interval } from '@nestjs/schedule'; +import { PrismaService } from '../prisma/prisma.service'; +import { NowPlayingService } from './now-playing.service'; + +// Ответ https://api3.unistar.by/client/latest/{slug} +interface UnistarLatest { + latest: { + channel_name?: string; + element_data?: { + Type?: string; // Music | Commercial | Program | Jingle | News... + Title?: string; + Artist?: string; + PictureFile?: string; + FullPictureUrl?: string; + }; + } | null; +} + +/** + * Now-playing для каналов Unistar (Беларусь). Все 8 каналов вещают через HLS + * (usp.unistar.by) — ICY-метаданных нет, поэтому трек берём из их API: + * GET https://api3.unistar.by/client/latest/{slug}, где slug = alt_name канала + * (unistar_main, unistar_top, ...), он же сегмент пути потока /hls/{slug}/master.m3u8. + * API отдаёт исполнителя, название и имя файла обложки (своя картинка трека). + */ +@Injectable() +export class UnistarNowPlayingService { + private readonly logger = new Logger(UnistarNowPlayingService.name); + // База для имён файлов обложек (pics_path из appData плеера Unistar) + private readonly picsBase = 'https://unistar.by/upload/music/photos/'; + private readonly headers = { + 'User-Agent': 'Mozilla/5.0', + Origin: 'https://unistar.by', + Referer: 'https://unistar.by/', + }; + + constructor( + private readonly prisma: PrismaService, + private readonly nowPlayingService: NowPlayingService, + ) {} + + @Interval(30000) + async pollUnistarNowPlaying() { + // Не фильтруем по isOnline: health-check ошибочно метит HLS-потоки offline. + const stations = await this.prisma.station.findMany({ + where: { streamUrl: { contains: 'unistar.by' } }, + }); + 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://api3.unistar.by/client/latest/${slug}`, + { headers: this.headers }, + ); + if (!res.ok) return; + + const data = (await res.json()) as UnistarLatest; + const el = data.latest?.element_data; + // Только музыка: рекламу/программы/джинглы не показываем как трек. + if (!el || el.Type !== 'Music') return; + + const artist = (el.Artist ?? '').trim(); + const song = (el.Title ?? '').trim(); + if (!artist || !song) return; + + await this.nowPlayingService.ingest({ + stationDbId: station.id, + stationNumericId: station.stationId, + artist, + song, + coverUrl: this.buildCoverUrl(el.PictureFile ?? el.FullPictureUrl), + }); + + if (!station.isOnline) { + await this.prisma.station.update({ + where: { id: station.id }, + data: { isOnline: true }, + }); + } + updated++; + }), + ); + + this.logger.log(`Unistar poll: ${updated}/${stations.length} обновлено`); + } + + // http://edge1.usp.unistar.by/hls/unistar_top/master.m3u8 → unistar_top + private extractSlug(streamUrl: string): string | null { + const m = streamUrl.match(/\/hls\/([a-z0-9_]+)\//i); + return m ? m[1].toLowerCase() : null; + } + + // Имя файла обложки → полный URL (или абсолютный URL как есть) + private buildCoverUrl(pic?: string): string | null { + const p = (pic ?? '').trim(); + if (!p) return null; + return p.startsWith('http') ? p : this.picsBase + p; + } +}