feat(now-playing): EMG (Европа Плюс и др.) now-playing через meta.hostingradio
Станции группы ЕМГ (emgsound.ru) получают текущий трек + готовую WebP-обложку
из единого meta.hostingradio.ru/emg/{slug}/history (slug из хоста потока,
order=desc → первый = сейчас). Заводится через NowPlayingService.ingest
(чарты + обогащение). ICY-поллер теперь пропускает emgsound (там HLS без ICY).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
100
src/now-playing/emg-now-playing.service.ts
Normal file
100
src/now-playing/emg-now-playing.service.ts
Normal file
@@ -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';
|
||||||
|
|
||||||
|
// Элемент истории meta.hostingradio.ru (нужные поля)
|
||||||
|
interface EmgHistoryItem {
|
||||||
|
artist?: string;
|
||||||
|
title?: string;
|
||||||
|
type?: number;
|
||||||
|
coverImageWebpUrl600?: string;
|
||||||
|
coverImageUrl600?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now-playing для станций группы ЕМГ (Европа Плюс, Ретро FM, Дорожное, Радио 7,
|
||||||
|
* Studio 21, Эльдорадио и их сабканалы). Все они вещают через emgsound.ru, а текущий
|
||||||
|
* трек с обложкой отдаёт единый сервис meta.hostingradio.ru/emg/{slug}/history.
|
||||||
|
* slug берём из хоста потока: hls-NN-{slug}.emgsound.ru. order=desc → первый = сейчас.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class EmgNowPlayingService {
|
||||||
|
private readonly logger = new Logger(EmgNowPlayingService.name);
|
||||||
|
private readonly headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0',
|
||||||
|
Origin: 'https://europaplus.ru',
|
||||||
|
Referer: 'https://europaplus.ru/',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly nowPlayingService: NowPlayingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Interval(30000)
|
||||||
|
async pollEmgNowPlaying() {
|
||||||
|
const stations = await this.prisma.station.findMany({
|
||||||
|
where: { isOnline: true, streamUrl: { contains: 'emgsound.ru' } },
|
||||||
|
});
|
||||||
|
if (stations.length === 0) return;
|
||||||
|
|
||||||
|
const { date, from, to } = this.mskWindow();
|
||||||
|
let updated = 0;
|
||||||
|
|
||||||
|
await Promise.allSettled(
|
||||||
|
stations.map(async (station) => {
|
||||||
|
const slug = this.extractSlug(station.streamUrl);
|
||||||
|
if (!slug) return;
|
||||||
|
|
||||||
|
const url =
|
||||||
|
`https://meta.hostingradio.ru/emg/${slug}/history` +
|
||||||
|
`?format=native&types=3&order=desc&date=${date}&from=${from}&to=${to}`;
|
||||||
|
const res = await fetch(url, { headers: this.headers });
|
||||||
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
const items = (await res.json()) as EmgHistoryItem[];
|
||||||
|
const cur = Array.isArray(items) ? items[0] : null;
|
||||||
|
if (!cur?.artist || !cur?.title) return;
|
||||||
|
|
||||||
|
await this.nowPlayingService.ingest({
|
||||||
|
stationDbId: station.id,
|
||||||
|
stationNumericId: station.stationId,
|
||||||
|
artist: cur.artist.trim(),
|
||||||
|
song: cur.title.trim(),
|
||||||
|
coverUrl: cur.coverImageWebpUrl600 ?? cur.coverImageUrl600 ?? null,
|
||||||
|
});
|
||||||
|
updated++;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`EMG poll: ${updated}/${stations.length} обновлено`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// hls-01-europaplus-kpop.emgsound.ru → europaplus-kpop
|
||||||
|
private extractSlug(streamUrl: string): string | null {
|
||||||
|
const m = streamUrl.match(/hls-\d+-([a-z0-9-]+)\.emgsound\.ru/i);
|
||||||
|
return m ? m[1].toLowerCase() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дата и окно времени по Москве (контейнер может быть в UTC)
|
||||||
|
private mskWindow(): { date: string; from: string; to: string } {
|
||||||
|
const fmt = (d: Date) => {
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Europe/Moscow',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23',
|
||||||
|
}).formatToParts(d);
|
||||||
|
const g = (t: string) => parts.find((p) => p.type === t)?.value ?? '00';
|
||||||
|
return { date: `${g('year')}-${g('month')}-${g('day')}`, time: `${g('hour')}:${g('minute')}` };
|
||||||
|
};
|
||||||
|
const now = new Date();
|
||||||
|
const cur = fmt(now);
|
||||||
|
const start = fmt(new Date(now.getTime() - 120 * 60 * 1000));
|
||||||
|
return { date: cur.date, from: start.time, to: cur.time };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,13 @@ export class IcyNowPlayingService {
|
|||||||
|
|
||||||
@Interval(60000)
|
@Interval(60000)
|
||||||
async pollIcyNowPlaying() {
|
async pollIcyNowPlaying() {
|
||||||
const where = { recordStationId: null, isOnline: true };
|
// emgsound (ЕМГ) обрабатывает EmgNowPlayingService через meta-API; их HLS-потоки
|
||||||
|
// ICY-метаданных не отдают — исключаем, чтобы не тратить слоты впустую.
|
||||||
|
const where = {
|
||||||
|
recordStationId: null,
|
||||||
|
isOnline: true,
|
||||||
|
NOT: { streamUrl: { contains: 'emgsound.ru' } },
|
||||||
|
};
|
||||||
const total = await this.prisma.station.count({ where });
|
const total = await this.prisma.station.count({ where });
|
||||||
if (total === 0) return;
|
if (total === 0) return;
|
||||||
if (this.cursor >= total) this.cursor = 0;
|
if (this.cursor >= total) this.cursor = 0;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NowPlayingController } from './now-playing.controller';
|
|||||||
import { NowPlayingService } from './now-playing.service';
|
import { NowPlayingService } from './now-playing.service';
|
||||||
import { RecordStationSyncService } from './record-station-sync.service';
|
import { RecordStationSyncService } from './record-station-sync.service';
|
||||||
import { IcyNowPlayingService } from './icy-now-playing.service';
|
import { IcyNowPlayingService } from './icy-now-playing.service';
|
||||||
|
import { EmgNowPlayingService } from './emg-now-playing.service';
|
||||||
import { ChartsModule } from '../charts/charts.module';
|
import { ChartsModule } from '../charts/charts.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -14,6 +15,7 @@ import { ChartsModule } from '../charts/charts.module';
|
|||||||
NowPlayingService,
|
NowPlayingService,
|
||||||
RecordStationSyncService,
|
RecordStationSyncService,
|
||||||
IcyNowPlayingService,
|
IcyNowPlayingService,
|
||||||
|
EmgNowPlayingService,
|
||||||
],
|
],
|
||||||
exports: [NowPlayingService],
|
exports: [NowPlayingService],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user