import { VIDEO_EXTENSIONS } from './const.js'; export function isVideo(filename) { return VIDEO_EXTENSIONS.some(ext => filename.toLowerCase().endsWith(ext) ); } export function extractInfoHash(magnetLink) { const match = magnetLink.match(/xt=urn:btih:([^&]+)/i); return match ? match[1].toLowerCase() : null; } export async function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } export function base64Encode(str) { return Buffer.from(str).toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } export function base64Decode(str) { str = str.replace(/-/g, '+').replace(/_/g, '/'); while (str.length % 4) str += '='; return Buffer.from(str, 'base64').toString('ascii'); } export function findBestFile(files, metadata) { if (!files?.length) return null; if (files.length === 1) return files[0]; // If no metadata provided, return largest file if (!metadata?.meta?.name) { const largestFile = files.sort((a, b) => (b.size || 0) - (a.size || 0))[0]; console.log('Selected largest file (no metadata available):', largestFile.name || largestFile.path); return largestFile; } const title = metadata.meta.name.toLowerCase(); const year = new Date(metadata.meta.released).getFullYear(); const yearStr = year.toString(); // Get key parts of the title (for Harry Potter and the Prisoner of Azkaban -> ["harry", "potter", "prisoner", "azkaban"]) const titleParts = title .replace(/[^a-z0-9\s]/g, '') .split(/\s+/) .filter(part => !['and', 'the', 'of'].includes(part)); console.log(`Looking for match with title parts:`, titleParts, `(${year})`); // Score each video file const scoredFiles = files.map(file => { let score = 0; const filename = file.name?.toLowerCase() || file.path?.toLowerCase() || ''; // Count matching title parts const matchingParts = titleParts.filter(part => filename.includes(part)); score += (matchingParts.length / titleParts.length) * 100; if (matchingParts.length > 0) { console.log(`Found ${matchingParts.length}/${titleParts.length} title parts in "${filename}"`); } // Year matching if (filename.includes(yearStr)) { score += 50; console.log(`Year match found in "${filename}"`); } // Penalize likely pack/collection files if (filename.match(/\b(pack|collection|complete|season|episode|[es]\d{2,3})\b/i)) { score -= 30; console.log(`Pack/collection penalty applied to "${filename}"`); } // Penalize extras/bonus content if (filename.match(/\b(extra|bonus|behind|feature|trailer|sample)\b/i)) { score -= 40; console.log(`Extras/bonus penalty applied to "${filename}"`); } return { file, score, size: file.size || 0, filename, matchingParts }; }); // Log all scores for debugging console.log('\nFile scores:'); scoredFiles.forEach(({filename, score, size, matchingParts}) => { console.log(`"${filename}": score=${score}, size=${(size / (1024 * 1024)).toFixed(2)}MB, matched=${matchingParts.join(', ')}`); }); // Sort first by score, then by size scoredFiles.sort((a, b) => { // If scores are significantly different, use score if (Math.abs(b.score - a.score) > 20) { return b.score - a.score; } // If scores are close, use file size return b.size - a.size; }); const bestMatch = scoredFiles[0]; // If no good matches found (low score), fall back to largest file if (bestMatch.score < 30) { console.log('No confident match found, falling back to largest file'); const largestFile = files.sort((a, b) => (b.size || 0) - (a.size || 0))[0]; console.log(`Selected largest file: ${largestFile.name || largestFile.path}`); return largestFile; } console.log(`\nSelected "${bestMatch.file.name || bestMatch.file.path}"`); console.log(`Score: ${bestMatch.score}`); console.log(`Size: ${(bestMatch.size / (1024 * 1024)).toFixed(2)}MB`); console.log(`Matched parts: ${bestMatch.matchingParts.join(', ')}`); return bestMatch.file; }