| 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 |
|
|
| |
| 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 } |
| } |
|
|
| |
| 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) { |
| |
| 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, |
| } |
|
|