feat(now-playing): now-playing + обложки для каналов Unistar
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 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ export class IcyNowPlayingService {
|
||||
'Love Radio',
|
||||
'Radio Monte Carlo',
|
||||
'Radio ROKS',
|
||||
'Unistar',
|
||||
],
|
||||
},
|
||||
NOT: { streamUrl: { contains: 'emgsound.ru' } },
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
106
src/now-playing/unistar-now-playing.service.ts
Normal file
106
src/now-playing/unistar-now-playing.service.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user