Compare commits

...

2 Commits

Author SHA1 Message Date
2c60c669c0 feat(api): turkcealtyazi aramasına sayfalama desteği ekle
Arama sonuçlarının birden fazla sayfa taranabilmesi için sayfalama
mekanizması eklendi. İlk sayfadan maksimum sayfa sayısı keşfedilir ve
her sayfa taranarak eşleşen film aranır. Sayfalar arası bekleme süresi
korunur ve boş sayfalarda işlem durdurulur. Maksimum 10 sayfa sınırı
eklendi.
2026-02-16 14:47:45 +03:00
14c64d8032 feat(watcher): polling desteği ekle
CORE_WATCHER_USE_POLLING ve CORE_WATCHER_POLL_INTERVAL_MS ortam
değişkenleri eklendi. Bu değişkenler sayesinde dosya izleyici için
polling modu ve aralığı yapılandırılabilir hale getirildi.
2026-02-16 14:47:26 +03:00
4 changed files with 86 additions and 11 deletions

View File

@@ -13,6 +13,8 @@ MEDIA_MOVIE_PATH=/media/movie
ENABLE_API_KEY=false ENABLE_API_KEY=false
API_KEY= API_KEY=
CORE_WATCHER_DEDUP_WINDOW_MS=15000 CORE_WATCHER_DEDUP_WINDOW_MS=15000
CORE_WATCHER_USE_POLLING=false
CORE_WATCHER_POLL_INTERVAL_MS=2000
ENABLE_TA_STEP_LOGS=false ENABLE_TA_STEP_LOGS=false
CLAMAV_AUTO_UPDATE=true CLAMAV_AUTO_UPDATE=true
CLAMAV_FAIL_ON_UPDATE_ERROR=false CLAMAV_FAIL_ON_UPDATE_ERROR=false

View File

@@ -149,7 +149,31 @@ function buildFindQuery(params: SearchParams): string {
return queryTokens.join(' '); return queryTokens.join(' ');
} }
function pickMovieLinkFromSearch(html: string, params: SearchParams, baseUrl: string): { movieUrl: string; movieTitle: string } | null { function buildSearchUrl(query: string, page: number): string {
if (page <= 1) return `${env.turkcealtyaziBaseUrl}/find.php?cat=sub&find=${encodeURIComponent(query)}`;
return `${env.turkcealtyaziBaseUrl}/find.php?cat=sub&find=${encodeURIComponent(query)}&p=${page}`;
}
function parseSearchMaxPage(html: string, baseUrl: string): number {
const $ = cheerio.load(html);
let maxPage = 1;
$('a[href]').each((_, el) => {
const href = ($(el).attr('href') || '').trim();
if (!href) return;
let parsedUrl: URL | null = null;
try {
parsedUrl = new URL(href, baseUrl);
} catch {
return;
}
if (parsedUrl.pathname !== '/find.php') return;
const p = Number(parsedUrl.searchParams.get('p') || '1');
if (Number.isFinite(p) && p > maxPage) maxPage = p;
});
return maxPage;
}
function extractMovieLinksFromSearch(html: string, params: SearchParams, baseUrl: string): Array<{ url: string; title: string; year?: number; score: number }> {
const $ = cheerio.load(html); const $ = cheerio.load(html);
const wantedYear = params.year; const wantedYear = params.year;
const wantedTitleTokens = tokenize(params.title); const wantedTitleTokens = tokenize(params.title);
@@ -207,7 +231,12 @@ function pickMovieLinkFromSearch(html: string, params: SearchParams, baseUrl: st
if (!prev || item.score > prev.score) dedup.set(item.url, item); if (!prev || item.score > prev.score) dedup.set(item.url, item);
} }
const ordered = [...dedup.values()].sort((a, b) => b.score - a.score); return [...dedup.values()].sort((a, b) => b.score - a.score);
}
function pickMovieLinkFromSearch(html: string, params: SearchParams, baseUrl: string): { movieUrl: string; movieTitle: string } | null {
const wantedYear = params.year;
const ordered = extractMovieLinksFromSearch(html, params, baseUrl);
if (ordered.length === 0) return null; if (ordered.length === 0) return null;
const best = ordered[0]; const best = ordered[0];
@@ -378,23 +407,57 @@ export async function searchTurkceAltyaziReal(params: SearchParams): Promise<Rea
const q = buildFindQuery(params); const q = buildFindQuery(params);
if (!q) return []; if (!q) return [];
const searchUrl = `${env.turkcealtyaziBaseUrl}/find.php?cat=sub&find=${encodeURIComponent(q)}`; const firstSearchUrl = buildSearchUrl(q, 1);
const cookies = new Map<string, string>(); const cookies = new Map<string, string>();
taInfo('TA_SEARCH_START', 'TurkceAltyazi search started', { taInfo('TA_SEARCH_START', 'TurkceAltyazi search started', {
title: params.title, title: params.title,
year: params.year, year: params.year,
release: params.release, release: params.release,
query: q, query: q,
searchUrl searchUrl: firstSearchUrl
}); });
try { try {
await sleep(env.turkcealtyaziMinDelayMs); const hardMaxPages = 10;
const searchRes = await getWithRetry(searchUrl, 2, cookies); let scannedPages = 0;
mergeCookies(cookies, searchRes.setCookie); let discoveredMaxPages = 1;
const pickedMovie = pickMovieLinkFromSearch(searchRes.body, params, env.turkcealtyaziBaseUrl); let pickedMovie: { movieUrl: string; movieTitle: string } | null = null;
for (let page = 1; page <= Math.min(discoveredMaxPages, hardMaxPages); page++) {
const searchUrl = buildSearchUrl(q, page);
await sleep(env.turkcealtyaziMinDelayMs);
const searchRes = await getWithRetry(searchUrl, 2, cookies);
mergeCookies(cookies, searchRes.setCookie);
scannedPages += 1;
if (page === 1) {
discoveredMaxPages = Math.max(1, parseSearchMaxPage(searchRes.body, env.turkcealtyaziBaseUrl));
}
const pageLinks = extractMovieLinksFromSearch(searchRes.body, params, env.turkcealtyaziBaseUrl);
taInfo('TA_SEARCH_PAGE_SCANNED', 'TurkceAltyazi search page scanned', {
page,
pageLinks: pageLinks.length,
discoveredMaxPages
});
// TA may return HTTP 200 with an empty list for out-of-range pages.
if (pageLinks.length === 0 && page > 1) {
taInfo('TA_SEARCH_PAGE_EMPTY_STOP', 'Search page has empty list, stopping pagination', { page });
break;
}
pickedMovie = pickMovieLinkFromSearch(searchRes.body, params, env.turkcealtyaziBaseUrl);
if (pickedMovie) break;
}
if (!pickedMovie) { if (!pickedMovie) {
taInfo('TA_SEARCH_RESULT', 'Movie page not matched from search list', { title: params.title, year: params.year, query: q }); taInfo('TA_SEARCH_RESULT', 'Movie page not matched from search list', {
title: params.title,
year: params.year,
query: q,
scannedPages
});
throw new PipelineError({ throw new PipelineError({
code: 'TA_MOVIE_NOT_MATCHED', code: 'TA_MOVIE_NOT_MATCHED',
message: `Movie not matched on search list (title=${params.title}, year=${params.year ?? 'n/a'})`, message: `Movie not matched on search list (title=${params.title}, year=${params.year ?? 'n/a'})`,

View File

@@ -15,5 +15,7 @@ export const env = {
enableApiKey: process.env.ENABLE_API_KEY === 'true', enableApiKey: process.env.ENABLE_API_KEY === 'true',
apiKey: process.env.API_KEY ?? '', apiKey: process.env.API_KEY ?? '',
watcherDedupWindowMs: Number(process.env.CORE_WATCHER_DEDUP_WINDOW_MS ?? 15000), watcherDedupWindowMs: Number(process.env.CORE_WATCHER_DEDUP_WINDOW_MS ?? 15000),
watcherUsePolling: process.env.CORE_WATCHER_USE_POLLING === 'true',
watcherPollIntervalMs: Number(process.env.CORE_WATCHER_POLL_INTERVAL_MS ?? 2000),
isDev: (process.env.NODE_ENV ?? 'development') !== 'production' isDev: (process.env.NODE_ENV ?? 'development') !== 'production'
}; };

View File

@@ -26,7 +26,13 @@ export async function startWatcher(): Promise<void> {
const byPath = new Map(watched.map((w) => [w.path, w.kind])); const byPath = new Map(watched.map((w) => [w.path, w.kind]));
const shouldProcessEvent = createEventDeduper(env.watcherDedupWindowMs); const shouldProcessEvent = createEventDeduper(env.watcherDedupWindowMs);
const watcher = chokidar.watch(paths, { ignoreInitial: false, awaitWriteFinish: false, persistent: true }); const watcher = chokidar.watch(paths, {
ignoreInitial: false,
awaitWriteFinish: false,
persistent: true,
usePolling: env.watcherUsePolling,
interval: env.watcherPollIntervalMs
});
watcher.on('add', async (p) => { watcher.on('add', async (p) => {
if (!isVideoFile(p)) return; if (!isVideoFile(p)) return;
@@ -51,7 +57,9 @@ export async function startWatcher(): Promise<void> {
await media.MediaFileModel.updateOne({ path: p }, { status: 'MISSING', lastSeenAt: new Date() }); await media.MediaFileModel.updateOne({ path: p }, { status: 'MISSING', lastSeenAt: new Date() });
}); });
console.log(`[core] watcher started for: ${paths.join(', ')}`); console.log(
`[core] watcher started for: ${paths.join(', ')} (polling=${env.watcherUsePolling ? 'on' : 'off'}, intervalMs=${env.watcherPollIntervalMs})`
);
} }
function resolveKind(filePath: string, byPath: Map<string, 'tv' | 'movie' | 'mixed'>): 'tv' | 'movie' { function resolveKind(filePath: string, byPath: Map<string, 'tv' | 'movie' | 'mixed'>): 'tv' | 'movie' {