feat(api): turkcealtyazi gerçek modunu stabil hale getir ve mock altyapısını kaldır

Mock fallback mantığını ve determinantik mock üretim kodlarını kaldırarak
TurkceAltyazi sağlayıcısını tamamen gerçek moda geçirdi. İyileştirilmiş
arama, indirme ve çerez yönetimi ile sağlam bir entegrasyon sağlandı.

- MockArtifact ve deterministic modüllerini kaldır
- TurkceAltyaziProvider'da mock fallback mantığını tamamen kaldır
- HTTP çerez yönetimi, retry mantığı ve hata işleme iyileştirmeleri
- ENABLE_TA_STEP_LOGS yapılandırması ile adım adım loglama
- TURKCEALTYAZI_ALLOW_MOCK_FALLBACK ortam değişkenini kaldır
- Dokümantasyonu gerçek mod reflektif olarak güncelle
- OpenSubtitles sağlayıcını gerçek entegrasyon tamamlanana kadar pasif yap
- Varsayılan kaynak etiketini 'mock' yerine 'unknown' olarak güncelle
This commit is contained in:
2026-02-16 10:50:59 +03:00
parent 0ba0cb1071
commit d38fc3b390
16 changed files with 374 additions and 298 deletions

View File

@@ -8,8 +8,8 @@ export const env = {
tempRoot: process.env.TEMP_ROOT ?? '/temp',
enableApiKey: process.env.ENABLE_API_KEY === 'true',
apiKey: process.env.API_KEY ?? '',
enableTaStepLogs: process.env.ENABLE_TA_STEP_LOGS === 'true',
enableTurkcealtyaziReal: process.env.ENABLE_TURKCEALTYAZI_REAL === 'true',
turkcealtyaziAllowMockFallback: process.env.TURKCEALTYAZI_ALLOW_MOCK_FALLBACK !== 'false',
turkcealtyaziBaseUrl: process.env.TURKCEALTYAZI_BASE_URL ?? 'https://turkcealtyazi.org',
turkcealtyaziTimeoutMs: Number(process.env.TURKCEALTYAZI_TIMEOUT_MS ?? 12000),
turkcealtyaziMinDelayMs: Number(process.env.TURKCEALTYAZI_MIN_DELAY_MS ?? 300)

View File

@@ -1,18 +0,0 @@
export function hashString(input: string): number {
let h = 2166136261;
for (let i = 0; i < input.length; i++) {
h ^= input.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
export function seeded(seed: number): () => number {
let t = seed;
return () => {
t += 0x6d2b79f5;
let x = Math.imul(t ^ (t >>> 15), t | 1);
x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
};
}

View File

@@ -1,51 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import AdmZip from 'adm-zip';
import type { Candidate, SearchParams } from '../types/index.js';
import { hashString, seeded } from './deterministic.js';
function buildSrt(title: string, season?: number, episode?: number): string {
const ep = season && episode ? ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}` : '';
return `1\n00:00:01,000 --> 00:00:04,000\n${title}${ep} satir 1\n\n2\n00:00:05,000 --> 00:00:08,000\n${title}${ep} satir 2\n\n3\n00:00:09,000 --> 00:00:12,000\n${title}${ep} satir 3\n`;
}
function buildAss(title: string): string {
return `[Script Info]\nTitle: ${title}\n[Events]\nDialogue: 0,0:00:01.00,0:00:04.00,Default,,0,0,0,,Ass satiri\n`;
}
export async function generateMockArtifact(candidate: Candidate, params: SearchParams, jobToken: string, downloadDir: string): Promise<{ type: 'archive' | 'direct'; filePath: string }> {
await fs.mkdir(downloadDir, { recursive: true });
const seed = hashString(`${jobToken}|${params.title}|${candidate.id}`);
const rnd = seeded(seed);
if (candidate.downloadType === 'direct') {
const filePath = path.join(downloadDir, `${candidate.id}.srt`);
await fs.writeFile(filePath, buildSrt(params.title, params.season, params.episode), 'utf8');
return { type: 'direct', filePath };
}
const zip = new AdmZip();
if (params.type === 'tv') {
const s = params.season ?? 1;
const e = params.episode ?? 1;
const base = params.title.replace(/\s+/g, '.');
zip.addFile(`${base}.S${String(s).padStart(2, '0')}E${String(e).padStart(2, '0')}.1080p.srt`, Buffer.from(buildSrt(params.title, s, e)));
zip.addFile(`${base}.S${String(s).padStart(2, '0')}E${String(e + 1).padStart(2, '0')}.srt`, Buffer.from(buildSrt(params.title, s, e + 1)));
zip.addFile(`${base}.S${String(s).padStart(2, '0')}E${String(Math.max(1, e - 1)).padStart(2, '0')}.srt`, Buffer.from(buildSrt(params.title, s, Math.max(1, e - 1))));
if (rnd() > 0.5) {
zip.addFile(`${base}.S${String(s).padStart(2, '0')}E${String(e).padStart(2, '0')}.ass`, Buffer.from(buildAss(params.title)));
}
} else {
const name = `${params.title.replace(/\s+/g, '.')}.${params.year ?? 2020}`;
zip.addFile(`${name}.tr.srt`, Buffer.from(buildSrt(params.title)));
if (rnd() > 0.3) {
zip.addFile(`${name}.txt`, Buffer.from('this is not subtitle'));
}
}
zip.addFile('invalid.bin', Buffer.from([0, 159, 255, 0, 18]));
const archivePath = path.join(downloadDir, `${candidate.id}.zip`);
zip.writeZip(archivePath);
return { type: 'archive', filePath: archivePath };
}

View File

@@ -60,7 +60,7 @@ export async function searchSubtitles(input: SearchParams) {
level: 'info',
step: 'TA_SEARCH_PARSED',
message: `TurkceAltyazi candidates parsed`,
meta: { total: c.length, real: realCount, mock: c.length - realCount }
meta: { total: c.length, real: realCount }
});
}
allCandidates.push(...c);

View File

@@ -0,0 +1,21 @@
import { env } from '../config/env.js';
function oneLine(input: unknown): string {
if (input === undefined || input === null) return '';
return String(input).replace(/\s+/g, ' ').trim();
}
export function taInfo(step: string, message: string, meta?: Record<string, unknown>) {
if (!env.enableTaStepLogs) return;
const base = `[TA] step=${oneLine(step)} msg="${oneLine(message)}"`;
const metaPart = meta ? ` meta=${oneLine(JSON.stringify(meta))}` : '';
console.log(`${base}${metaPart}`);
}
export function taError(step: string, error: unknown, meta?: Record<string, unknown>) {
if (!env.enableTaStepLogs) return;
const reason = error instanceof Error ? error.message : oneLine(error);
const base = `[TA][ERROR] step=${oneLine(step)} reason="${oneLine(reason)}"`;
const metaPart = meta ? ` meta=${oneLine(JSON.stringify(meta))}` : '';
console.error(`${base}${metaPart}`);
}

View File

@@ -1,8 +1,10 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
import { URL } from 'node:url';
import { Buffer } from 'node:buffer';
import { env } from '../config/env.js';
import type { SearchParams } from '../types/index.js';
import { taError, taInfo } from './taLog.js';
export interface RealTaCandidate {
id: string;
@@ -29,15 +31,64 @@ function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getWithRetry(url: string, retries = 2): Promise<string> {
interface HttpResultText {
body: string;
finalUrl: string;
setCookie: string[];
}
interface HttpResultBinary {
body: Buffer;
finalUrl: string;
setCookie: string[];
contentType?: string;
}
function parseSetCookie(setCookie: string[]): Map<string, string> {
const out = new Map<string, string>();
for (const raw of setCookie) {
const first = raw.split(';')[0]?.trim();
if (!first) continue;
const idx = first.indexOf('=');
if (idx <= 0) continue;
const k = first.slice(0, idx).trim();
const v = first.slice(idx + 1).trim();
if (k) out.set(k, v);
}
return out;
}
function cookieHeader(cookies: Map<string, string>): string {
return [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
}
function mergeCookies(target: Map<string, string>, setCookie: string[]) {
const parsed = parseSetCookie(setCookie);
for (const [k, v] of parsed.entries()) target.set(k, v);
}
async function getWithRetry(url: string, retries = 2, cookies?: Map<string, string>): Promise<HttpResultText> {
let lastError: unknown;
for (let i = 0; i <= retries; i++) {
try {
if (i > 0) await sleep(250 * i);
const res = await client.get(url);
return typeof res.data === 'string' ? res.data : String(res.data);
taInfo('HTTP_GET_START', 'HTTP GET started', { url, attempt: i + 1, retries: retries + 1 });
const res = await client.get(url, {
headers: cookies && cookies.size > 0 ? { cookie: cookieHeader(cookies) } : undefined
});
taInfo('HTTP_GET_RESULT', 'HTTP GET completed', {
url,
finalUrl: (res.request as any)?.res?.responseUrl || url,
contentType: res.headers['content-type']
});
return {
body: typeof res.data === 'string' ? res.data : String(res.data),
finalUrl: (res.request as any)?.res?.responseUrl || url,
setCookie: Array.isArray(res.headers['set-cookie']) ? res.headers['set-cookie'] : []
};
} catch (err) {
lastError = err;
taError('HTTP_GET_FAILED', err, { url, attempt: i + 1, retries: retries + 1 });
}
}
throw lastError;
@@ -56,110 +107,252 @@ function abs(base: string, maybeRelative: string): string {
return new URL(maybeRelative, base).toString();
}
function parseCandidateNodes(html: string, baseUrl: string): RealTaCandidate[] {
function normalizeText(input: string): string {
return input
.toLowerCase()
.replace(/ç/g, 'c')
.replace(/ğ/g, 'g')
.replace(/ı/g, 'i')
.replace(/ö/g, 'o')
.replace(/ş/g, 's')
.replace(/ü/g, 'u')
.replace(/[^a-z0-9\s]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function tokenize(input: string): string[] {
return normalizeText(input)
.split(/\s+/)
.filter(Boolean);
}
function buildFindQuery(params: SearchParams): string {
const toks = tokenize(params.title).filter((t) => !/^\d+$/.test(t));
return toks.slice(0, 2).join(' ');
}
function pickMovieLinkFromSearch(html: string, params: SearchParams, baseUrl: string): { movieUrl: string; movieTitle: string } | null {
const $ = cheerio.load(html);
const results: RealTaCandidate[] = [];
const wantedYear = params.year;
const wantedTitleTokens = tokenize(params.title);
const links: Array<{ url: string; title: string; year?: number; score: number }> = [];
$('a[href]').each((_, el) => {
$('a[href^="/mov/"]').each((_, el) => {
const href = ($(el).attr('href') || '').trim();
const text = $(el).text().replace(/\s+/g, ' ').trim();
if (!href) return;
if (!href || text.length < 3) return;
const looksLikeSubtitle = /(altyazi|subtitle|sub|s\d{1,2}e\d{1,2}|\b\d{4}\b)/i.test(text + ' ' + href);
if (!looksLikeSubtitle) return;
const title = ($(el).attr('title') || $(el).text() || '').replace(/\s+/g, ' ').trim();
if (!title) return;
const full = abs(baseUrl, href);
if (!/turkcealtyazi\.org/i.test(full)) return;
const containerText = ($(el).closest('div').parent().text() || '').replace(/\s+/g, ' ').trim();
const yearMatch = containerText.match(/\((19\d{2}|20\d{2})\)/);
const year = yearMatch ? Number(yearMatch[1]) : undefined;
const id = `ta-real-${Buffer.from(full).toString('base64').slice(0, 18)}`;
const lowered = (text + ' ' + href).toLowerCase();
const titleTokens = tokenize(title);
const overlap = wantedTitleTokens.filter((t) => titleTokens.includes(t)).length;
let score = overlap;
if (wantedYear && year === wantedYear) score += 10;
results.push({
id,
title: text,
detailUrl: full,
lang: /\btr\b|turkce|türkçe/i.test(lowered) ? 'tr' : 'tr',
releaseHints: normalizeReleaseHints(text),
isHI: /\bhi\b|isitme|hearing/i.test(lowered),
isForced: /forced|zorunlu/i.test(lowered)
links.push({
url: abs(baseUrl, href),
title,
year,
score
});
});
const uniq = new Map<string, RealTaCandidate>();
for (const r of results) {
if (!uniq.has(r.detailUrl)) uniq.set(r.detailUrl, r);
const dedup = new Map<string, { url: string; title: string; year?: number; score: number }>();
for (const item of links) {
const prev = dedup.get(item.url);
if (!prev || item.score > prev.score) dedup.set(item.url, item);
}
return [...uniq.values()].slice(0, 12);
const ordered = [...dedup.values()].sort((a, b) => b.score - a.score);
if (ordered.length === 0) return null;
const best = ordered[0];
if (wantedYear && best.year && best.year !== wantedYear) return null;
return { movieUrl: best.url, movieTitle: best.title };
}
function pickSubPageFromMovieDetail(html: string, movieUrl: string, params: SearchParams): { subUrl: string; title: string; releaseHints: string[]; isHI: boolean } | null {
const $ = cheerio.load(html);
const wantedRelease = normalizeText(params.release || '');
const rows = $('.altsonsez2');
const candidates: Array<{ subUrl: string; title: string; releaseHints: string[]; isHI: boolean; score: number }> = [];
rows.each((_, row) => {
const linkEl = $(row).find('a[href^="/sub/"]').first();
const href = (linkEl.attr('href') || '').trim();
if (!href) return;
const title = (linkEl.text() || '').replace(/\s+/g, ' ').trim() || (linkEl.attr('title') || '').trim();
const ripText = ($(row).find('.ripdiv').text() || '').replace(/\s+/g, ' ').trim();
const relHints = normalizeReleaseHints(ripText);
const normalizedRip = normalizeText(ripText);
const isHI = /(sdh|hearing|isitme|hi)/i.test(ripText);
let score = 0;
if (wantedRelease) {
if (normalizedRip.includes(wantedRelease)) score += 20;
const releaseToken = wantedRelease.split(/\s+/).find(Boolean);
if (releaseToken && normalizedRip.includes(releaseToken)) score += 15;
} else {
score += 1;
}
if ($(row).find('.flagtr').length > 0) score += 3;
candidates.push({
subUrl: abs(movieUrl, href),
title,
releaseHints: relHints,
isHI,
score
});
});
if (candidates.length === 0) return null;
const picked = candidates.sort((a, b) => b.score - a.score)[0];
if (wantedRelease && picked.score < 10) return null;
return picked;
}
export async function searchTurkceAltyaziReal(params: SearchParams): Promise<RealTaCandidate[]> {
const q = [params.title, params.year, params.type === 'tv' ? `S${String(params.season ?? 1).padStart(2, '0')}E${String(params.episode ?? 1).padStart(2, '0')}` : '']
.filter(Boolean)
.join(' ');
if (params.type !== 'movie') return [];
const q = buildFindQuery(params);
if (!q) return [];
const candidatesPages = [
`${env.turkcealtyaziBaseUrl}/arama?q=${encodeURIComponent(q)}`,
`${env.turkcealtyaziBaseUrl}/find.php?cat=sub&find=${encodeURIComponent(q)}`
];
const merged: RealTaCandidate[] = [];
for (const url of candidatesPages) {
try {
await sleep(env.turkcealtyaziMinDelayMs);
const html = await getWithRetry(url, 2);
merged.push(...parseCandidateNodes(html, env.turkcealtyaziBaseUrl));
if (merged.length >= 8) break;
} catch {
// bir sonraki endpoint denenecek
}
}
const uniq = new Map<string, RealTaCandidate>();
for (const item of merged) {
if (!uniq.has(item.detailUrl)) uniq.set(item.detailUrl, item);
}
return [...uniq.values()].slice(0, 10);
}
export async function resolveTurkceAltyaziDownloadUrl(detailUrl: string): Promise<string> {
await sleep(env.turkcealtyaziMinDelayMs);
const html = await getWithRetry(detailUrl, 2);
const $ = cheerio.load(html);
const linkCandidates: string[] = [];
$('a[href]').each((_, el) => {
const href = ($(el).attr('href') || '').trim();
const text = $(el).text().trim();
if (!href) return;
const looksDownload = /(indir|download|\.zip|\.rar|\.7z|\.srt|\.ass)/i.test(`${href} ${text}`);
if (!looksDownload) return;
linkCandidates.push(abs(detailUrl, href));
const searchUrl = `${env.turkcealtyaziBaseUrl}/find.php?cat=sub&find=${encodeURIComponent(q)}`;
const cookies = new Map<string, string>();
taInfo('TA_SEARCH_START', 'TurkceAltyazi search started', {
title: params.title,
year: params.year,
release: params.release,
query: q,
searchUrl
});
const preferred =
linkCandidates.find((l) => /\.(zip|rar|7z)(\?|$)/i.test(l)) ||
linkCandidates.find((l) => /\.(srt|ass)(\?|$)/i.test(l)) ||
linkCandidates[0];
try {
await sleep(env.turkcealtyaziMinDelayMs);
const searchRes = await getWithRetry(searchUrl, 2, cookies);
mergeCookies(cookies, searchRes.setCookie);
const pickedMovie = pickMovieLinkFromSearch(searchRes.body, params, env.turkcealtyaziBaseUrl);
if (!pickedMovie) {
taInfo('TA_SEARCH_RESULT', 'Movie page not matched from search list', { title: params.title, year: params.year, query: q });
return [];
}
taInfo('TA_MOVIE_SELECTED', 'Movie detail page selected', { movieUrl: pickedMovie.movieUrl, movieTitle: pickedMovie.movieTitle });
if (!preferred) {
throw new Error('TA detail page download link parse failed');
await sleep(env.turkcealtyaziMinDelayMs);
const movieRes = await getWithRetry(pickedMovie.movieUrl, 2, cookies);
mergeCookies(cookies, movieRes.setCookie);
const pickedSub = pickSubPageFromMovieDetail(movieRes.body, pickedMovie.movieUrl, params);
if (!pickedSub) {
taInfo('TA_SEARCH_RESULT', 'Subtitle sub-page not matched by release', {
movieUrl: pickedMovie.movieUrl,
release: params.release
});
return [];
}
taInfo('TA_SUB_SELECTED', 'Subtitle sub-page selected', {
subUrl: pickedSub.subUrl,
releaseHints: pickedSub.releaseHints
});
const id = `ta-real-${Buffer.from(pickedSub.subUrl).toString('base64').slice(0, 18)}`;
const result = [{
id,
title: pickedSub.title || pickedMovie.movieTitle,
detailUrl: pickedSub.subUrl,
lang: 'tr',
releaseHints: pickedSub.releaseHints,
isHI: pickedSub.isHI,
isForced: false
}];
taInfo('TA_SEARCH_RESULT', 'TurkceAltyazi search completed', { candidateCount: result.length, subUrl: pickedSub.subUrl });
return result;
} catch (err) {
taError('TA_SEARCH_FAILED', err, { title: params.title, year: params.year, release: params.release, query: q });
throw err;
}
return preferred;
}
export async function downloadTurkceAltyaziFile(url: string): Promise<{ buffer: Buffer; finalUrl: string; contentType?: string }> {
await sleep(env.turkcealtyaziMinDelayMs);
const res = await client.get<ArrayBuffer>(url, { responseType: 'arraybuffer' });
const buffer = Buffer.from(res.data);
return {
buffer,
finalUrl: (res.request as any)?.res?.responseUrl || url,
contentType: res.headers['content-type']
};
function parseDownloadForm(html: string): { idid: string; altid: string; sidid: string } | null {
const $ = cheerio.load(html);
const idid = ($('input[name="idid"]').attr('value') || '').trim();
const altid = ($('input[name="altid"]').attr('value') || '').trim();
const sidid = ($('input[name="sidid"]').attr('value') || '').trim();
if (!idid || !altid || !sidid) return null;
return { idid, altid, sidid };
}
async function postIndWithRetry(subPageUrl: string, payload: { idid: string; altid: string; sidid: string }, cookies: Map<string, string>, retries = 2): Promise<HttpResultBinary> {
let lastError: unknown;
for (let i = 0; i <= retries; i++) {
try {
if (i > 0) await sleep(250 * i);
const form = new URLSearchParams(payload).toString();
const indUrl = `${env.turkcealtyaziBaseUrl}/ind`;
taInfo('TA_IND_POST_START', 'POST /ind started', { subPageUrl, indUrl, attempt: i + 1, retries: retries + 1, altid: payload.altid });
const res = await client.post<ArrayBuffer>(indUrl, form, {
responseType: 'arraybuffer',
headers: {
'content-type': 'application/x-www-form-urlencoded',
origin: env.turkcealtyaziBaseUrl,
referer: subPageUrl,
cookie: cookieHeader(cookies)
}
});
return {
body: Buffer.from(res.data),
finalUrl: (res.request as any)?.res?.responseUrl || indUrl,
setCookie: Array.isArray(res.headers['set-cookie']) ? res.headers['set-cookie'] : [],
contentType: res.headers['content-type']
};
} catch (err) {
lastError = err;
taError('TA_IND_POST_FAILED', err, { subPageUrl, attempt: i + 1, retries: retries + 1 });
}
}
throw lastError;
}
export async function downloadTurkceAltyaziFile(subPageUrl: string): Promise<{ buffer: Buffer; finalUrl: string; contentType?: string }> {
const cookies = new Map<string, string>();
taInfo('TA_DOWNLOAD_START', 'TurkceAltyazi subtitle download started', { subPageUrl });
try {
await sleep(env.turkcealtyaziMinDelayMs);
const subPageRes = await getWithRetry(subPageUrl, 2, cookies);
mergeCookies(cookies, subPageRes.setCookie);
const form = parseDownloadForm(subPageRes.body);
if (!form) {
const err = new Error('TA sub page download form parse failed');
taError('TA_FORM_PARSE_FAILED', err, { subPageUrl });
throw err;
}
taInfo('TA_FORM_PARSED', 'Download form parsed', { subPageUrl, altid: form.altid, idid: form.idid });
await sleep(env.turkcealtyaziMinDelayMs);
const res = await postIndWithRetry(subPageUrl, form, cookies, 2);
mergeCookies(cookies, res.setCookie);
taInfo('TA_DOWNLOAD_RESULT', 'Subtitle download completed', {
subPageUrl,
finalUrl: res.finalUrl,
contentType: res.contentType,
bytes: res.body.byteLength
});
return {
buffer: res.body,
finalUrl: res.finalUrl,
contentType: res.contentType
};
} catch (err) {
taError('TA_DOWNLOAD_FAILED', err, { subPageUrl });
throw err;
}
}

View File

@@ -1,45 +1,12 @@
import type { Candidate, SearchParams, SubtitleProvider } from '../types/index.js';
import { generateMockArtifact } from '../lib/mockArtifact.js';
import { hashString, seeded } from '../lib/deterministic.js';
import { env } from '../config/env.js';
import type { Candidate, DownloadedArtifact, SearchParams, SubtitleProvider } from '../types/index.js';
export class OpenSubtitlesProvider implements SubtitleProvider {
async search(params: SearchParams): Promise<Candidate[]> {
// TODO(v2): real OpenSubtitles API integration.
const key = `${params.title}|${params.year}|${params.season}|${params.episode}|os`;
const rnd = seeded(hashString(key));
const base = params.title.replace(/\s+/g, '.');
const directForMovie = params.type === 'movie' && rnd() > 0.4;
return [
{
id: `os-${hashString(`${key}-a`)}`,
provider: 'opensubtitles',
displayName: `OS ${base} Official`,
downloadType: directForMovie ? 'direct' : 'archiveZip',
downloadUrl: directForMovie ? `mock://os/${base}/direct.srt` : `mock://os/${base}/archive.zip`,
lang: 'tr',
releaseHints: ['1080p', rnd() > 0.5 ? 'x265' : 'x264', 'flux'],
scoreHints: ['api_match'],
isHI: rnd() > 0.8,
isForced: rnd() > 0.92
},
{
id: `os-${hashString(`${key}-b`)}`,
provider: 'opensubtitles',
displayName: `OS ${base} Backup`,
downloadType: 'archiveZip',
downloadUrl: `mock://os/${base}/backup.zip`,
lang: 'tr',
releaseHints: ['720p', 'x264'],
scoreHints: ['backup'],
isHI: false,
isForced: false
}
];
async search(_params: SearchParams): Promise<Candidate[]> {
// Real OpenSubtitles entegrasyonu tamamlanana kadar provider pasif.
return [];
}
async download(candidate: Candidate, params: SearchParams, jobToken: string) {
const artifact = await generateMockArtifact(candidate, params, jobToken, `${env.tempRoot}/${jobToken}/download`);
return { type: artifact.type, filePath: artifact.filePath, candidateId: candidate.id };
async download(_candidate: Candidate, _params: SearchParams, _jobToken: string): Promise<DownloadedArtifact> {
throw new Error('OpenSubtitles real download not implemented');
}
}

View File

@@ -1,14 +1,12 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import type { Candidate, SearchParams, SubtitleProvider } from '../types/index.js';
import { generateMockArtifact } from '../lib/mockArtifact.js';
import { hashString, seeded } from '../lib/deterministic.js';
import type { Candidate, DownloadedArtifact, SearchParams, SubtitleProvider } from '../types/index.js';
import { env } from '../config/env.js';
import {
downloadTurkceAltyaziFile,
resolveTurkceAltyaziDownloadUrl,
searchTurkceAltyaziReal
} from '../lib/turkcealtyaziReal.js';
import { taError, taInfo } from '../lib/taLog.js';
function extensionFromDownload(url: string, contentType?: string): 'zip' | 'rar' | '7z' | 'srt' | 'ass' {
const lowerUrl = url.toLowerCase();
@@ -22,84 +20,72 @@ function extensionFromDownload(url: string, contentType?: string): 'zip' | 'rar'
export class TurkceAltyaziProvider implements SubtitleProvider {
async search(params: SearchParams): Promise<Candidate[]> {
if (env.enableTurkcealtyaziReal) {
try {
const real = await searchTurkceAltyaziReal(params);
if (real.length > 0) {
return real.map((item, index) => ({
id: item.id || `ta-real-${index}`,
provider: 'turkcealtyazi',
displayName: item.title,
downloadType: 'archiveZip',
downloadUrl: item.detailUrl,
lang: item.lang || 'tr',
releaseHints: item.releaseHints,
scoreHints: ['real_provider'],
isHI: item.isHI,
isForced: item.isForced
}));
}
} catch (err) {
if (!env.turkcealtyaziAllowMockFallback) {
throw err;
}
}
}
if (!env.enableTurkcealtyaziReal) return [];
const key = `${params.title}|${params.year}|${params.season}|${params.episode}|ta`;
const rnd = seeded(hashString(key));
const base = params.title.replace(/\s+/g, '.');
return [
{
id: `ta-${hashString(`${key}-a`)}`,
taInfo('TA_PROVIDER_SEARCH_START', 'Provider search started', {
title: params.title,
year: params.year,
release: params.release
});
try {
const real = await searchTurkceAltyaziReal(params);
taInfo('TA_PROVIDER_SEARCH_RESULT', 'Provider search completed', { candidateCount: real.length });
return real.map((item, index) => ({
id: item.id || `ta-real-${index}`,
provider: 'turkcealtyazi',
displayName: `TA ${base} Ana Surum`,
displayName: item.title,
downloadType: 'archiveZip',
downloadUrl: `mock://ta/${base}/a.zip`,
lang: 'tr',
releaseHints: [rnd() > 0.4 ? '1080p' : '720p', 'x265', 'flux'],
scoreHints: ['trusted', 'crowd'],
isHI: rnd() > 0.7,
isForced: false
},
{
id: `ta-${hashString(`${key}-b`)}`,
provider: 'turkcealtyazi',
displayName: `TA ${base} Alternatif`,
downloadType: 'archiveZip',
downloadUrl: `mock://ta/${base}/b.zip`,
lang: 'tr',
releaseHints: ['webrip', 'x264'],
scoreHints: ['alt'],
isHI: false,
isForced: false
}
];
downloadUrl: item.detailUrl,
lang: item.lang || 'tr',
releaseHints: item.releaseHints,
scoreHints: ['real_provider'],
isHI: item.isHI,
isForced: item.isForced
}));
} catch (err) {
taError('TA_PROVIDER_SEARCH_FAILED', err, { title: params.title, year: params.year, release: params.release });
throw err;
}
}
async download(candidate: Candidate, params: SearchParams, jobToken: string) {
if (env.enableTurkcealtyaziReal && /^https?:\/\//i.test(candidate.downloadUrl)) {
const downloadDir = `${env.tempRoot}/${jobToken}/download`;
await fs.mkdir(downloadDir, { recursive: true });
const trace: Array<{ level: 'info' | 'warn' | 'error'; step: string; message: string; meta?: any }> = [];
async download(candidate: Candidate, _params: SearchParams, jobToken: string): Promise<DownloadedArtifact> {
if (!/^https?:\/\//i.test(candidate.downloadUrl)) {
throw new Error('TurkceAltyazi candidate download URL must be http(s)');
}
trace.push({ level: 'info', step: 'TA_DETAIL_FETCHED', message: candidate.downloadUrl });
const resolved = await resolveTurkceAltyaziDownloadUrl(candidate.downloadUrl);
trace.push({ level: 'info', step: 'TA_DOWNLOAD_URL_RESOLVED', message: resolved });
const downloaded = await downloadTurkceAltyaziFile(resolved);
const downloadDir = `${env.tempRoot}/${jobToken}/download`;
await fs.mkdir(downloadDir, { recursive: true });
const trace: Array<{ level: 'info' | 'warn' | 'error'; step: string; message: string; meta?: any }> = [];
taInfo('TA_PROVIDER_DOWNLOAD_START', 'Provider download started', {
candidateId: candidate.id,
subUrl: candidate.downloadUrl,
jobToken
});
try {
trace.push({ level: 'info', step: 'TA_SUB_PAGE_FETCHED', message: candidate.downloadUrl });
const downloaded = await downloadTurkceAltyaziFile(candidate.downloadUrl);
trace.push({ level: 'info', step: 'TA_IND_POST_DONE', message: downloaded.finalUrl });
const ext = extensionFromDownload(downloaded.finalUrl, downloaded.contentType);
const filePath = path.join(downloadDir, `${candidate.id}.${ext}`);
await fs.writeFile(filePath, downloaded.buffer);
const type: 'direct' | 'archive' = ext === 'srt' || ext === 'ass' ? 'direct' : 'archive';
taInfo('TA_PROVIDER_DOWNLOAD_RESULT', 'Provider download completed', {
candidateId: candidate.id,
filePath,
type,
ext
});
return {
type: ext === 'srt' || ext === 'ass' ? 'direct' : 'archive',
type,
filePath,
candidateId: candidate.id,
trace
};
} catch (err) {
taError('TA_PROVIDER_DOWNLOAD_FAILED', err, { candidateId: candidate.id, subUrl: candidate.downloadUrl, jobToken });
throw err;
}
const artifact = await generateMockArtifact(candidate, params, jobToken, `${env.tempRoot}/${jobToken}/download`);
return { type: artifact.type, filePath: artifact.filePath, candidateId: candidate.id };
}
}