fix(charts): отсев мусора и разговорных/шуточных станций из чарта

recordPlay теперь не считает: разговорные/шуточные жанры (Кассиопея, Юмор ФМ,
Рассказы, Радио Вера, Comedy Radio, ВГТРК, Старое радио) и мусорные названия
(хекс-плейсхолдеры с цифрой, URL, числовые коды, artist==song). Исторический мусор
почищен напрямую в БД (−4273 трека, −13274 плея талк/комеди-станций).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-06 15:49:17 +03:00
parent 38e380a59f
commit 0084177d15

View File

@@ -2,6 +2,18 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { EnrichmentService } from '../enrich/enrichment.service';
// Жанры, исключённые из чарта: разговорные/шуточные/без названий треков.
// Их «треки» — это названия передач/реприз/спектаклей, не музыка.
const EXCLUDED_CHART_GENRES = [
'Станция Кассиопея',
'Юмор ФМ',
'Рассказы',
'Радио Вера',
'Comedy Radio',
'ВГТРК',
'Старое радио',
];
// Период чарта
export type ChartPeriod = 'day' | 'week' | 'month' | 'all';
@@ -109,6 +121,35 @@ export class ChartsService {
return this.stationNames;
}
// Кэш id станций исключённых жанров (разговорные/шуточные — не в чарт)
private excludedStationIds = new Set<string>();
private excludedStationIdsAt = 0;
private async getExcludedStationIds(): Promise<Set<string>> {
const now = Date.now();
if (now - this.excludedStationIdsAt > 10 * 60 * 1000 || this.excludedStationIds.size === 0) {
const rows = await this.prisma.station.findMany({
where: { genre: { in: EXCLUDED_CHART_GENRES } },
select: { id: true },
});
this.excludedStationIds = new Set(rows.map((r) => r.id));
this.excludedStationIdsAt = now;
}
return this.excludedStationIds;
}
// Мусорный «трек»: хекс-плейсхолдер, URL, числовой код, или artist == song
// (целый тайтл без нормального разбиения на исполнителя/название).
private isJunkTrack(artist: string, song: string): boolean {
const a = artist.trim();
const s = song.trim();
if (a.toLowerCase() === s.toLowerCase()) return true;
const hex = (v: string) => /^[0-9a-f]{6,}$/i.test(v) && /[0-9]/.test(v);
const code = (v: string) => /^[0-9]+-[0-9]+/.test(v);
const url = (v: string) => /\.(ru|fm|by|com|ua)$/i.test(v) || /^https?:/i.test(v);
return [a, s].some((v) => hex(v) || code(v) || url(v));
}
// Записывает факт смены трека на станции (вызывается из NowPlayingService)
async recordPlay(params: RecordPlayParams): Promise<void> {
try {
@@ -119,6 +160,11 @@ export class ChartsService {
// Отсекаем не-музыкальные записи: пустые поля, либо когда артист/песня
// совпадает с названием станции (это джинглы, шоу, сетевые промо).
if (!artist || !song) return;
// Разговорные/шуточные станции — не в чарт.
const excluded = await this.getExcludedStationIds();
if (excluded.has(stationDbId)) return;
// Мусорные названия (хекс/URL/код/artist==song) — не в чарт.
if (this.isJunkTrack(artist, song)) return;
const stationNames = await this.getStationNames();
if (
stationNames.has(artist.toLowerCase()) ||