diff --git a/.forgejo/scripts/check_m3u_links.js b/.forgejo/scripts/check_m3u_links.js index b79eafe..a9c666b 100644 --- a/.forgejo/scripts/check_m3u_links.js +++ b/.forgejo/scripts/check_m3u_links.js @@ -1,6 +1,6 @@ /** - * M3U Playlist Dead Link Checker - * Checks URLs in an M3U playlist and logs dead links for Forgejo Actions. + * M3U Live Stream Dead Link Checker + * Checks live stream URLs in an M3U playlist and logs dead streams for Forgejo Actions. */ const fs = require('fs'); @@ -8,17 +8,18 @@ const https = require('https'); const http = require('http'); const { URL } = require('url'); -// Configuration -const M3U_FILE = 'mystique.m3u'; -const REQUEST_TIMEOUT = 10000; // 10 seconds -const MAX_RETRIES = 2; -const DELAY_BETWEEN_REQUESTS = 1000; // 1 second -const USER_AGENT = 'M3U-Checker/1.0'; - class M3UChecker { + constructor() { + // Configuration + this.M3U_FILE = 'mystique.m3u'; + this.REQUEST_TIMEOUT = 10000; // 10 seconds + this.MAX_RETRIES = 2; + this.DELAY_BETWEEN_REQUESTS = 1000; // 1 second + this.USER_AGENT = 'M3U-Checker/1.0'; + } /** - * Parse M3U file and extract URLs with metadata + * Parse M3U file and extract stream URLs with metadata */ parseM3U(filepath) { if (!fs.existsSync(filepath)) { @@ -33,22 +34,46 @@ class M3UChecker { for (const line of lines) { if (line.startsWith('#EXTINF:')) { - // Parse EXTINF line: #EXTINF:duration,title - const match = line.match(/^#EXTINF:([^,]*),(.+)$/); - if (match) { - currentExtinf = { - duration: match[1].trim(), - title: match[2].trim() - }; + // Parse EXTINF line with better regex to handle attributes + // Format: #EXTINF:duration attribute="value" attribute="value",title + const extinfMatch = line.match(/^#EXTINF:(.+)$/); + if (extinfMatch) { + const extinfContent = extinfMatch[1]; + + // Find the last comma which separates attributes from title + const lastCommaIndex = extinfContent.lastIndexOf(','); + + if (lastCommaIndex !== -1) { + const attributesPart = extinfContent.substring(0, lastCommaIndex).trim(); + const title = extinfContent.substring(lastCommaIndex + 1).trim(); + + // Extract group-title if present + const groupMatch = attributesPart.match(/group-title="([^"]+)"/); + const groupTitle = groupMatch ? groupMatch[1] : ''; + + currentExtinf = { + title: title, + groupTitle: groupTitle + }; + } else { + // Fallback for malformed EXTINF + currentExtinf = { + title: extinfContent, + groupTitle: '' + }; + } } else { - currentExtinf = { duration: '', title: 'Unknown' }; + currentExtinf = { + title: 'Unknown', + groupTitle: '' + }; } } else if (!line.startsWith('#') && this.isValidUrl(line)) { - // This should be a URL + // This should be a stream URL const entry = { url: line, title: currentExtinf ? currentExtinf.title : 'Unknown', - duration: currentExtinf ? currentExtinf.duration : '' + groupTitle: currentExtinf ? currentExtinf.groupTitle : '' }; entries.push(entry); currentExtinf = null; @@ -71,11 +96,11 @@ class M3UChecker { } /** - * Check if URL is accessible + * Check if stream URL is accessible * Returns Promise<{isAlive: boolean, error: string, statusCode: number}> */ async checkUrl(url) { - for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) { try { const result = await this.makeRequest(url); if (result.statusCode < 400) { @@ -84,11 +109,11 @@ class M3UChecker { return { isAlive: false, error: `HTTP ${result.statusCode}`, statusCode: result.statusCode }; } } catch (error) { - if (attempt === MAX_RETRIES - 1) { + if (attempt === this.MAX_RETRIES - 1) { return { isAlive: false, error: error.message, statusCode: 0 }; } // Wait before retry - await this.sleep(DELAY_BETWEEN_REQUESTS * (attempt + 1)); + await this.sleep(this.DELAY_BETWEEN_REQUESTS * (attempt + 1)); } } } @@ -107,9 +132,9 @@ class M3UChecker { port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: 'HEAD', - timeout: REQUEST_TIMEOUT, + timeout: this.REQUEST_TIMEOUT, headers: { - 'User-Agent': USER_AGENT + 'User-Agent': this.USER_AGENT } }; @@ -124,7 +149,7 @@ class M3UChecker { req.on('timeout', () => { req.destroy(); - reject(new Error(`Timeout after ${REQUEST_TIMEOUT}ms`)); + reject(new Error(`Timeout after ${this.REQUEST_TIMEOUT}ms`)); }); req.on('error', (error) => { @@ -157,9 +182,9 @@ class M3UChecker { port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: 'GET', - timeout: REQUEST_TIMEOUT, + timeout: this.REQUEST_TIMEOUT, headers: { - 'User-Agent': USER_AGENT, + 'User-Agent': this.USER_AGENT, 'Range': 'bytes=0-1024' } }; @@ -174,7 +199,7 @@ class M3UChecker { req.on('timeout', () => { req.destroy(); - reject(new Error(`Timeout after ${REQUEST_TIMEOUT}ms`)); + reject(new Error(`Timeout after ${this.REQUEST_TIMEOUT}ms`)); }); req.on('error', (error) => { @@ -193,80 +218,88 @@ class M3UChecker { } /** - * Log dead link information + * Log dead stream information */ logDeadLink(entry, error, statusCode) { - console.log(`\nšŸ”“ DEAD LINK FOUND:`); + console.log(`\nšŸ”“ DEAD STREAM FOUND:`); console.log(` Title: ${entry.title}`); + if (entry.groupTitle) { + console.log(` Group: ${entry.groupTitle}`); + } console.log(` URL: ${entry.url}`); console.log(` Error: ${error}`); console.log(` Status Code: ${statusCode || 'N/A'}`); - console.log(` Duration: ${entry.duration || 'Unknown'}`); } /** - * Main function to check M3U playlist + * Main function to check M3U live streams */ async runCheck() { - console.log(`šŸŽµ M3U Playlist Dead Link Checker`); + console.log(`šŸŽµ M3U Live Stream Dead Link Checker`); console.log(`=====================================`); try { - console.log(`šŸ“‚ Parsing ${M3U_FILE}...`); - const entries = this.parseM3U(M3U_FILE); - console.log(`šŸ” Found ${entries.length} URLs to check\n`); + console.log(`šŸ“‚ Parsing ${this.M3U_FILE}...`); + const entries = this.parseM3U(this.M3U_FILE); + console.log(`šŸ” Found ${entries.length} streams to check\n`); if (entries.length === 0) { - console.log('āŒ No URLs found in playlist'); + console.log('āŒ No stream URLs found in playlist'); return; } - const deadLinks = []; + const deadStreams = []; let aliveCount = 0; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const progress = `[${(i + 1).toString().padStart(3)}/${entries.length}]`; - console.log(`${progress} Checking: ${entry.title}`); + // Show group title in progress if available + const displayTitle = entry.groupTitle ? + `${entry.title} (${entry.groupTitle})` : entry.title; + console.log(`${progress} Checking: ${displayTitle}`); const result = await this.checkUrl(entry.url); if (result.isAlive) { aliveCount++; - console.log(` āœ… Alive (${result.statusCode})`); + console.log(` āœ… Live (${result.statusCode})`); } else { - deadLinks.push({ entry, error: result.error, statusCode: result.statusCode }); + deadStreams.push({ entry, error: result.error, statusCode: result.statusCode }); console.log(` āŒ Dead (${result.error})`); - // Log dead link details immediately + // Log dead stream details immediately this.logDeadLink(entry, result.error, result.statusCode); } // Add delay between requests if (i < entries.length - 1) { - await this.sleep(DELAY_BETWEEN_REQUESTS); + await this.sleep(this.DELAY_BETWEEN_REQUESTS); } } // Final summary console.log(`\nšŸ“Š CHECK COMPLETED`); console.log(`===================`); - console.log(`āœ… Alive: ${aliveCount}`); - console.log(`āŒ Dead: ${deadLinks.length}`); + console.log(`āœ… Live: ${aliveCount}`); + console.log(`āŒ Dead: ${deadStreams.length}`); console.log(`šŸ“ˆ Success Rate: ${((aliveCount / entries.length) * 100).toFixed(1)}%`); - if (deadLinks.length > 0) { - console.log(`\n🚨 DEAD LINKS SUMMARY:`); - console.log(`=======================`); - deadLinks.forEach(({ entry, error }, index) => { + if (deadStreams.length > 0) { + console.log(`\n🚨 DEAD STREAMS SUMMARY:`); + console.log(`=========================`); + deadStreams.forEach(({ entry, error }, index) => { console.log(`${(index + 1).toString().padStart(2)}. ${entry.title}`); + if (entry.groupTitle) { + console.log(` Group: ${entry.groupTitle}`); + } console.log(` URL: ${entry.url}`); console.log(` Error: ${error}`); console.log(''); }); } else { - console.log(`\nšŸŽ‰ All links are alive! No issues found.`); + console.log(`\nšŸŽ‰ All streams are live! No issues found.`); } } catch (error) {