Update .forgejo/scripts/check_m3u_links.js
This commit is contained in:
parent
19162dc7bc
commit
e6733b52b9
1 changed files with 85 additions and 52 deletions
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue