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