const fs = require('fs') const path = require('path') const LIB_PATH = path.join(__dirname, '..', 'config', 'patternLibrary.json') let _cache = null function load() { if (_cache) return _cache try { _cache = JSON.parse(fs.readFileSync(LIB_PATH, 'utf-8')) return _cache } catch { _cache = { version: 1, patterns: [] } return _cache } } function save() { if (!_cache) return fs.writeFileSync(LIB_PATH, JSON.stringify(_cache, null, 2), 'utf-8') } function matchByMediaType(mediaType) { const lib = load() return lib.patterns.filter(p => p.mediaType.includes(mediaType) && p.type !== 'search') } function matchBySite(baseUrl) { const lib = load() const domain = new URL(baseUrl).hostname.replace(/^www\./, '') return lib.patterns.filter(p => p.sites.some(s => domain.includes(s.replace(/^www\./, ''))) ) } function matchByExtractionPattern(html, limit = 3) { const lib = load() const hints = [] for (const p of lib.patterns) { const method = p.extraction?.method || '' const selector = p.extraction?.selector || '' if (selector && html.includes(selector)) { hints.push({ patternId: p.id, matchType: 'selector', confidence: 'high', pattern: p }) } if (method === 'iframe_regex' && /video\[\d+\]/.test(html)) { hints.push({ patternId: p.id, matchType: 'regex', confidence: 'high', pattern: p }) } if (method === 'xorscrape' && html.includes('data-server')) { hints.push({ patternId: p.id, matchType: 'data_attr', confidence: 'medium', pattern: p }) } if (method === 'dooplayer_options' && html.includes('dooplay_player_option')) { hints.push({ patternId: p.id, matchType: 'class', confidence: 'high', pattern: p }) } if (method === 'dooplayer_ajax' && html.includes('dooplayer')) { hints.push({ patternId: p.id, matchType: 'keyword', confidence: 'medium', pattern: p }) } } return hints.slice(0, limit) } function learnFromFix(providerName, baseUrl, mediaType, urlPattern, extractionMethod, htmlSample) { const lib = load() const domain = new URL(baseUrl).hostname // Check if this site already has this pattern const existing = lib.patterns.find(p => p.sites.some(s => domain.includes(s.replace(/^www\./, ''))) && p.urlTmpl === urlPattern ) if (existing) { existing.successCount = (existing.successCount || 1) + 1 if (!existing.sites.includes(domain)) existing.sites.push(domain) save() return { action: 'updated', pattern: existing } } // New pattern: extract as much as possible const newPattern = { id: `${mediaType}-${Date.now()}`, urlTmpl: urlPattern, mediaType: [mediaType], slugSource: 'unknown', extraction: { method: extractionMethod || 'unknown', step: 'auto_learned', hint: 'Pattern auto-learned from successful repair', }, sites: [domain], successCount: 1, autoLearned: true, learnedAt: new Date().toISOString(), } lib.patterns.push(newPattern) save() return { action: 'added', pattern: newPattern } } function getDirectPatterns(mediaType) { return matchByMediaType(mediaType) } function getDirectFromTitle(mediaType) { const lib = load() return lib.patterns.filter(p => p.mediaType.includes(mediaType) && p.type !== 'search' && (p.slugSource === 'title' || !p.slugSource) ) } function getSearchPatterns(mediaType) { const lib = load() return lib.patterns.filter(p => p.mediaType.includes(mediaType) && p.type === 'search') } function getAllForProvider(baseUrl, mediaType) { const bySite = matchBySite(baseUrl) const byType = matchByMediaType(mediaType) const merged = [...bySite, ...byType] const seen = new Set() return merged.filter(p => { if (seen.has(p.id)) return false seen.add(p.id) return true }) } function fillUrlTemplate(tmpl, vars) { // vars: { slug, id, season, episode, query, year } let url = tmpl .replace(/{slug}/g, vars.slug || '') .replace(/{id}/g, String(vars.id || '')) .replace(/{animeId}/g, String(vars.animeId || vars.id || '')) .replace(/{season}/g, String(vars.season || '')) .replace(/{episode}/g, String(vars.episode || '')) .replace(/{groupSlug}/g, vars.groupSlug || '') .replace(/{query}/g, encodeURIComponent(vars.query || '')) .replace(/{title}/g, encodeURIComponent(vars.title || '')) .replace(/{year}/g, String(vars.year || '')) return url } module.exports = { load, save, matchByMediaType, matchBySite, matchByExtractionPattern, learnFromFix, getDirectPatterns, getDirectFromTitle, getSearchPatterns, getAllForProvider, fillUrlTemplate, }