From 0dd8f60626f7d71b0cd1592a1d9b86901ec06e0c Mon Sep 17 00:00:00 2001 From: szbk Date: Mon, 16 Feb 2026 17:05:51 +0300 Subject: [PATCH] =?UTF-8?q?feat(api):=20fps=20tabanl=C4=B1=20altyaz=C4=B1?= =?UTF-8?q?=20e=C5=9Fle=C5=9Ftirmesi=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Video ve altyazı FPS değerlerini karşılaştırarak daha doğru eşleştirme yapar. Tam eşleşme ve token eşleşmesi bulunamadığında FPS uyumlu altyazıları önceliklendirir. FFprobe çıktısından FPS değerini normalize eder ve karşılaştırma için kullanır. --- services/api/src/lib/turkcealtyaziReal.ts | 96 ++++++++++++++++++----- services/core/src/utils/ffprobe.ts | 24 +++++- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/services/api/src/lib/turkcealtyaziReal.ts b/services/api/src/lib/turkcealtyaziReal.ts index 3f7e5d1..b64f5c9 100644 --- a/services/api/src/lib/turkcealtyaziReal.ts +++ b/services/api/src/lib/turkcealtyaziReal.ts @@ -16,7 +16,7 @@ export interface RealTaCandidate { releaseHints: string[]; isHI: boolean; isForced: boolean; - strategy?: 'exact' | 'token' | 'fallback' | 'default' | 'package_fallback'; + strategy?: 'exact' | 'token' | 'fps' | 'fallback' | 'default' | 'package_fallback'; isPackage?: boolean; } @@ -113,6 +113,34 @@ function normalizeReleaseHints(raw: string): string[] { .slice(0, 10); } +function normalizeFpsText(raw?: string | number | null): string | null { + if (raw === undefined || raw === null) return null; + const s = String(raw).trim().replace(/,/g, '.').replace(/\s+/g, ''); + if (!s) return null; + + if (s.includes('/')) { + const [a, b] = s.split('/'); + const n = Number(a); + const d = Number(b); + if (!Number.isFinite(n) || !Number.isFinite(d) || d === 0) return null; + const v = Number((n / d).toFixed(3)); + return Number.isInteger(v) ? String(v) : String(v); + } + + const v = Number(s); + if (!Number.isFinite(v)) return null; + const rounded = Number(v.toFixed(3)); + return Number.isInteger(rounded) ? String(rounded) : String(rounded); +} + +function fpsEquals(a?: string | null, b?: string | null): boolean { + if (!a || !b) return false; + const an = Number(a); + const bn = Number(b); + if (!Number.isFinite(an) || !Number.isFinite(bn)) return false; + return Math.abs(an - bn) <= 0.01; +} + function abs(base: string, maybeRelative: string): string { return new URL(maybeRelative, base).toString(); } @@ -268,7 +296,8 @@ function pickSubPageFromMovieDetail( title: string; releaseHints: string[]; isHI: boolean; - strategy: 'exact' | 'token' | 'fallback' | 'default' | 'package_fallback'; + strategy: 'exact' | 'token' | 'fps' | 'fallback' | 'default' | 'package_fallback'; + fps?: string | null; isPackage?: boolean; }; noMatchReason?: 'episode_not_matched' | 'release_not_matched' | 'no_sub_rows'; @@ -276,6 +305,11 @@ function pickSubPageFromMovieDetail( const $ = cheerio.load(html); const wantedRelease = normalizeText(params.release || ''); const wantedReleaseTokens = wantedRelease.split(/\s+/).filter(Boolean); + const wantedFps = normalizeFpsText( + params.mediaInfo?.video?.fps ?? + params.mediaInfo?.video?.avg_frame_rate ?? + params.mediaInfo?.video?.r_frame_rate + ); const wantedSeason = params.type === 'tv' ? params.season : undefined; const wantedEpisode = params.type === 'tv' ? params.episode : undefined; const rows = $('[class*="altsonsez"]'); @@ -292,6 +326,7 @@ function pickSubPageFromMovieDetail( season?: number; episode?: number; isPackage: boolean; + fps?: string | null; }> = []; rows.each((_, row) => { @@ -307,6 +342,7 @@ function pickSubPageFromMovieDetail( const isTr = $(row).find('.flagtr').length > 0; const indirmeRaw = ($(row).find('.alindirme').text() || '').replace(/\./g, '').replace(/,/g, '').trim(); const downloadCount = Number(indirmeRaw.replace(/[^\d]/g, '')) || 0; + const fps = normalizeFpsText(($(row).find('.alfps').text() || '').trim()); const { season, episode, isPackage } = parseSeasonEpisodeFromRow($, row); if (params.type === 'tv') { @@ -341,7 +377,8 @@ function pickSubPageFromMovieDetail( downloadCount, season, episode, - isPackage + isPackage, + fps }); }); @@ -366,28 +403,39 @@ function pickSubPageFromMovieDetail( } } - if (!wantedRelease || forcedStrategy === 'package_fallback') { - const picked = selectedPool.sort((a, b) => b.score - a.score || b.downloadCount - a.downloadCount)[0]; - if (forcedStrategy === 'package_fallback') { - return { picked: { ...picked, strategy: 'package_fallback', isPackage: true } }; + if (wantedRelease && forcedStrategy !== 'package_fallback') { + const exact = selectedPool + .filter((c) => c.releaseExact) + .sort((a, b) => b.score - a.score || b.downloadCount - a.downloadCount)[0]; + if (exact) { + return { picked: { ...exact, strategy: 'exact' } }; } + + const token = selectedPool + .filter((c) => c.releaseTokenHits > 0) + .sort((a, b) => b.releaseTokenHits - a.releaseTokenHits || b.score - a.score || b.downloadCount - a.downloadCount)[0]; + if (token) { + return { picked: { ...token, strategy: 'token' } }; + } + } + + const fpsMatched = wantedFps + ? selectedPool.find((c) => fpsEquals(c.fps || null, wantedFps)) + : undefined; + if (fpsMatched) { + return { picked: { ...fpsMatched, strategy: 'fps' } }; + } + + if (forcedStrategy === 'package_fallback') { + const picked = selectedPool.sort((a, b) => b.score - a.score || b.downloadCount - a.downloadCount)[0]; + return { picked: { ...picked, strategy: 'package_fallback', isPackage: true } }; + } + + if (!wantedRelease) { + const picked = selectedPool.sort((a, b) => b.score - a.score || b.downloadCount - a.downloadCount)[0]; return { picked: { ...picked, strategy: 'default' } }; } - const exact = selectedPool - .filter((c) => c.releaseExact) - .sort((a, b) => b.score - a.score || b.downloadCount - a.downloadCount)[0]; - if (exact) { - return { picked: { ...exact, strategy: 'exact' } }; - } - - const token = selectedPool - .filter((c) => c.releaseTokenHits > 0) - .sort((a, b) => b.releaseTokenHits - a.releaseTokenHits || b.score - a.score || b.downloadCount - a.downloadCount)[0]; - if (token) { - return { picked: { ...token, strategy: 'token' } }; - } - if (params.type === 'tv') { // TV'de once bolum dogrulugu, sonra release gelir. Release bulunamasa da en iyi bolum satirini kullan. const tvFallback = selectedPool @@ -502,6 +550,12 @@ export async function searchTurkceAltyaziReal(params: SearchParams): Promise { const { stdout } = await execFileAsync('ffprobe', [ '-v', @@ -25,7 +44,8 @@ export async function analyzeWithFfprobe(path: string): Promise { codec_name: video.codec_name, width: video.width, height: video.height, - r_frame_rate: video.r_frame_rate + r_frame_rate: video.r_frame_rate, + fps: normalizeFpsValue(video.avg_frame_rate || video.r_frame_rate) } : null, audio, @@ -39,7 +59,7 @@ export async function analyzeWithFfprobe(path: string): Promise { export function fallbackMediaInfo(): any { return { - video: { codec_name: 'unknown', width: 1920, height: 1080, r_frame_rate: '24/1' }, + video: { codec_name: 'unknown', width: 1920, height: 1080, r_frame_rate: '24/1', fps: 24 }, audio: [{ codec_name: 'unknown', channels: 2, language: 'und' }], format: { duration: '0', bit_rate: '0', format_name: 'matroska' } };