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; } }