| const { getDb, scheduleSave } = require('./db') |
|
|
| const FRESH_DAYS = 30 |
| const domainCache = new Map() |
|
|
| function parseUrl(url) { |
| try { |
| const u = new URL(url) |
| return { domain: u.origin, path: u.pathname + u.search + u.hash } |
| } catch { |
| return { domain: '', path: url } |
| } |
| } |
|
|
| async function getDomainId(db, domain) { |
| if (domainCache.has(domain)) return domainCache.get(domain) |
|
|
| const stmt = db.prepare(`SELECT id FROM domains WHERE domain = ?`) |
| stmt.bind([domain]) |
| const found = stmt.step() |
| if (found) { |
| const id = stmt.getAsObject().id |
| stmt.free() |
| domainCache.set(domain, id) |
| return id |
| } |
| stmt.free() |
|
|
| try { |
| db.run(`INSERT INTO domains (domain) VALUES (?)`, [domain]) |
| } catch { |
| const retryStmt = db.prepare(`SELECT id FROM domains WHERE domain = ?`) |
| retryStmt.bind([domain]) |
| retryStmt.step() |
| const id = retryStmt.getAsObject().id |
| retryStmt.free() |
| domainCache.set(domain, id) |
| return id |
| } |
| const r = db.exec('SELECT last_insert_rowid()') |
| const id = r[0].values[0][0] |
| domainCache.set(domain, id) |
| return id |
| } |
|
|
| async function getFreshStreams(tmdbId, season, episode) { |
| const db = await getDb() |
| let where = `sr.tmdb_id = ? AND sr.is_alive = 1 AND sr.last_seen > datetime('now', '-' || ? || ' days')` |
| const params = [String(tmdbId), FRESH_DAYS] |
|
|
| if (season != null && episode != null) { |
| where += ` AND sr.season = ? AND sr.episode = ?` |
| params.push(season, episode) |
| } else { |
| where += ` AND sr.season IS NULL AND sr.episode IS NULL` |
| } |
|
|
| const stmt = db.prepare(` |
| SELECT sr.*, d.domain || sr.url_path as url |
| FROM stream_results sr |
| JOIN domains d ON d.id = sr.domain_id |
| WHERE ${where} |
| ORDER BY sr.quality DESC |
| `) |
| stmt.bind(params) |
| const results = [] |
| while (stmt.step()) { |
| results.push(stmt.getAsObject()) |
| } |
| stmt.free() |
| return results |
| } |
|
|
| async function getAllStreams(tmdbId, season, episode) { |
| const db = await getDb() |
| let where = `sr.tmdb_id = ? AND sr.is_alive = 1` |
| const params = [String(tmdbId)] |
|
|
| if (season != null && episode != null) { |
| where += ` AND sr.season = ? AND sr.episode = ?` |
| params.push(season, episode) |
| } else { |
| where += ` AND sr.season IS NULL AND sr.episode IS NULL` |
| } |
|
|
| const stmt = db.prepare(` |
| SELECT sr.*, d.domain || sr.url_path as url |
| FROM stream_results sr |
| JOIN domains d ON d.id = sr.domain_id |
| WHERE ${where} |
| ORDER BY sr.last_seen DESC |
| `) |
| stmt.bind(params) |
| const results = [] |
| while (stmt.step()) { |
| results.push(stmt.getAsObject()) |
| } |
| stmt.free() |
| return results |
| } |
|
|
| async function logStreams(tmdbId, provider, contentType, streams, title, season, episode) { |
| const db = await getDb() |
| const now = new Date().toISOString() |
| const seasonVal = season != null ? season : null |
| const episodeVal = episode != null ? episode : null |
|
|
| const checkStmt = db.prepare(` |
| SELECT sr.id FROM stream_results sr |
| JOIN domains d ON d.id = sr.domain_id |
| WHERE d.domain = ? AND sr.url_path = ? AND sr.provider = ? AND sr.tmdb_id = ? |
| AND sr.season IS ? AND sr.episode IS ? |
| `) |
| const insertStmt = db.prepare(` |
| INSERT INTO stream_results (domain_id, url_path, provider, content_type, tmdb_id, season, episode, title, quality, language, created_at, last_seen) |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `) |
| const updateStmt = db.prepare(` |
| UPDATE stream_results SET last_seen = ?, is_alive = 1 |
| WHERE domain_id = ? AND url_path = ? AND provider = ? AND tmdb_id = ? |
| AND season IS ? AND episode IS ? |
| `) |
|
|
| for (const s of streams) { |
| const { domain, path } = parseUrl(s.url) |
| const domainId = await getDomainId(db, domain) |
|
|
| checkStmt.bind([domain, path, provider, String(tmdbId), seasonVal, episodeVal]) |
| const exists = checkStmt.step() |
|
|
| if (exists) { |
| updateStmt.run([now, domainId, path, provider, String(tmdbId), seasonVal, episodeVal]) |
| } else { |
| insertStmt.run([ |
| domainId, |
| path, |
| provider, |
| contentType, |
| String(tmdbId), |
| seasonVal, |
| episodeVal, |
| title || s.title || null, |
| s.quality || null, |
| s.language || null, |
| now, |
| now, |
| ]) |
| } |
| } |
| checkStmt.free() |
| insertStmt.free() |
| updateStmt.free() |
| scheduleSave() |
| } |
|
|
| async function markDead(url, provider) { |
| const db = await getDb() |
| const { domain, path } = parseUrl(url) |
| db.run(` |
| DELETE FROM stream_results |
| WHERE domain_id = (SELECT id FROM domains WHERE domain = ?) |
| AND url_path = ? AND provider = ? |
| `, [domain, path, provider || '']) |
| scheduleSave() |
| } |
|
|
| async function markAlive(url, provider) { |
| const db = await getDb() |
| const { domain, path } = parseUrl(url) |
| const now = new Date().toISOString() |
| db.run(` |
| UPDATE stream_results |
| SET is_alive = 1, last_seen = ?, last_validated = ? |
| WHERE domain_id = (SELECT id FROM domains WHERE domain = ?) |
| AND url_path = ? AND provider = ? |
| `, [now, now, domain, path, provider || '']) |
| scheduleSave() |
| } |
|
|
| async function getProvidersForTmdb(tmdbId) { |
| const db = await getDb() |
| const stmt = db.prepare(` |
| SELECT DISTINCT provider FROM stream_results |
| WHERE tmdb_id = ? AND is_alive = 1 |
| `) |
| stmt.bind([String(tmdbId)]) |
| const providers = [] |
| while (stmt.step()) { |
| providers.push(stmt.getAsObject().provider) |
| } |
| stmt.free() |
| return providers |
| } |
|
|
| async function getStats() { |
| const db = await getDb() |
|
|
| const total = db.exec(`SELECT COUNT(*) FROM stream_results WHERE is_alive = 1`) |
| const totalStreams = total.length > 0 ? total[0].values[0][0] : 0 |
|
|
| const totalDomains = db.exec(`SELECT COUNT(*) FROM domains`) |
| const domainsCount = totalDomains.length > 0 ? totalDomains[0].values[0][0] : 0 |
|
|
| const byProvider = db.exec(` |
| SELECT provider, |
| COUNT(*) as total, |
| SUM(CASE WHEN is_alive = 1 THEN 1 ELSE 0 END) as alive, |
| MAX(last_seen) as last_seen |
| FROM stream_results |
| GROUP BY provider |
| ORDER BY total DESC |
| `) |
|
|
| const recentChecks = db.exec(` |
| SELECT COUNT(*) FROM stream_checks |
| WHERE checked_at > datetime('now', '-24 hours') |
| `) |
| const checks24h = recentChecks.length > 0 ? recentChecks[0].values[0][0] : 0 |
|
|
| const stale = db.exec(` |
| SELECT COUNT(*) FROM stream_results |
| WHERE is_alive = 1 AND last_seen < datetime('now', '-${FRESH_DAYS} days') |
| `) |
| const staleCount = stale.length > 0 ? stale[0].values[0][0] : 0 |
|
|
| const providers = [] |
| if (byProvider.length > 0) { |
| for (const row of byProvider[0].values) { |
| providers.push({ |
| provider: row[0], |
| total: row[1], |
| alive: row[2], |
| last_seen: row[3], |
| }) |
| } |
| } |
|
|
| return { |
| totalStreams, |
| domainsCount, |
| staleCount, |
| checks24h, |
| providers, |
| } |
| } |
|
|
| async function logCheck(url, result) { |
| const db = await getDb() |
| db.run(` |
| INSERT INTO stream_checks (url, checked_at, response_ms, status_code, is_alive, error) |
| VALUES (?, datetime('now'), ?, ?, ?, ?) |
| `, [url, result.responseMs || null, result.statusCode || null, result.isAlive ? 1 : 0, result.error || null]) |
| scheduleSave() |
| } |
|
|
| async function deleteStale(maxAgeDays) { |
| const db = await getDb() |
| db.run(` |
| DELETE FROM stream_results |
| WHERE last_seen < datetime('now', '-' || ? || ' days') |
| AND is_alive = 0 |
| `, [maxAgeDays || 90]) |
| scheduleSave() |
| } |
|
|
| async function pruneChecks(maxAgeDays) { |
| const db = await getDb() |
| db.run(` |
| DELETE FROM stream_checks |
| WHERE checked_at < datetime('now', '-' || ? || ' days') |
| `, [maxAgeDays || 90]) |
| scheduleSave() |
| } |
|
|
| async function pruneOrphanDomains() { |
| const db = await getDb() |
| db.run(` |
| DELETE FROM domains |
| WHERE id NOT IN (SELECT DISTINCT domain_id FROM stream_results) |
| `) |
| scheduleSave() |
| } |
|
|
| module.exports = { |
| getFreshStreams, |
| getAllStreams, |
| logStreams, |
| markDead, |
| markAlive, |
| getProvidersForTmdb, |
| getStats, |
| logCheck, |
| deleteStale, |
| pruneChecks, |
| pruneOrphanDomains, |
| } |
|
|