diff --git a/.forgejo/scripts/cultural-groups.js b/.forgejo/scripts/cultural-groups.js new file mode 100644 index 0000000..8e6cd2b --- /dev/null +++ b/.forgejo/scripts/cultural-groups.js @@ -0,0 +1,188 @@ +const fs = require('fs'); +const path = require('path'); + +// Define cultural groups by country +const culturalGroups = { + anglosphere: [ + 'united kingdom', 'uk', 'britain', 'england', 'scotland', 'wales', 'northern ireland', + 'united states', 'usa', 'america', 'canada', 'australia', 'new zealand', 'ireland' + ], + francophone: [ + 'france', 'belgium', 'switzerland', 'quebec', 'monaco', + 'luxembourg', 'haiti', 'ivory coast', 'senegal', 'cameroon' + ], + hispanic: [ + 'spain', 'mexico', 'argentina', 'chile', 'colombia', 'peru', + 'venezuela', 'ecuador', 'guatemala', 'cuba', 'dominican republic', + 'honduras', 'el salvador', 'nicaragua', 'costa rica', 'panama' + ], + lusophone: [ + 'portugal', 'brazil', 'angola', 'mozambique', + 'cape verde', 'guinea-bissau', 'sao tome and principe' + ], + arabic: [ + 'saudi arabia', 'egypt', 'uae', 'united arab emirates', 'qatar', + 'kuwait', 'oman', 'bahrain', 'yemen', 'iraq', 'syria', + 'jordan', 'lebanon', 'palestine', 'libya', 'tunisia', + 'algeria', 'morocco', 'sudan' + ], + germanosphere: [ + 'germany', 'austria', 'switzerland', 'luxembourg', 'liechtenstein' + ], + slavic: [ + 'russia', 'ukraine', 'belarus', 'poland', 'czech republic', + 'slovakia', 'serbia', 'croatia', 'bosnia', 'montenegro', + 'slovenia', 'bulgaria', 'north macedonia' + ], + sinosphere: [ + 'china', 'hong kong', 'taiwan', 'singapore', 'macau' + ], + indosphere: [ + 'india', 'pakistan', 'bangladesh', 'nepal', 'sri lanka', + 'bhutan', 'maldives' + ], + turkic: [ + 'turkey', 'azerbaijan', 'uzbekistan', 'kazakhstan', + 'kyrgyzstan', 'turkmenistan' + ], + nordic: [ + 'sweden', 'norway', 'denmark', 'finland', 'iceland', + 'faroe islands', 'greenland' + ], + baltic: [ + 'estonia', 'latvia', 'lithuania' + ], + hellenic: [ + 'greece', 'cyprus' + ], + benelux: [ + 'netherlands', 'belgium', 'luxembourg' + ], + persian: [ + 'iran', 'afghanistan', 'tajikistan' + ], + malaysphere: [ + 'malaysia', 'brunei', 'indonesia' + ], + korean: [ + 'south korea', 'korea', 'north korea' + ], + japanese: [ + 'japan' + ], + vietnamese: [ + 'vietnam' + ], + thai: [ + 'thailand' + ] +}; + +function getCulturalGroup(channelInfo) { + // Look specifically for group-title (country information) + const groupMatch = channelInfo.match(/group-title="([^"]*)"/) || []; + const groupTitle = (groupMatch[1] || '').toLowerCase(); + + // Check if the country belongs to any cultural group + for (const [group, countries] of Object.entries(culturalGroups)) { + if (countries.some(country => groupTitle.includes(country))) { + return group; + } + } + + return null; // Return null instead of 'other' for non-matching channels +} + +function splitByCulturalGroup(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + + // Create cultural groups directory + const groupsDir = path.join(path.dirname(filePath), 'cultural-groups'); + if (!fs.existsSync(groupsDir)) { + fs.mkdirSync(groupsDir); + } + + // Get existing cultural group files + const existingFiles = fs.readdirSync(groupsDir) + .filter(file => file.endsWith('.m3u')) + .map(file => file.toLowerCase()); + + const groups = {}; + let currentExtinf = null; + + // Verify M3U header + const header = lines[0]; + if (!header.startsWith('#EXTM3U')) { + throw new Error('Invalid M3U file: Missing #EXTM3U header'); + } + + // Process lines + lines.forEach(line => { + line = line.trim(); + if (!line) return; + + if (line.startsWith('#EXTINF')) { + currentExtinf = line; + const culturalGroup = getCulturalGroup(line); + + // Only add to a group if there's a match + if (culturalGroup) { + if (!groups[culturalGroup]) { + groups[culturalGroup] = ['#EXTM3U']; + } + groups[culturalGroup].push(line); + } + } else if (currentExtinf && !line.startsWith('#')) { + const culturalGroup = getCulturalGroup(currentExtinf); + // Only add the URL line if the channel belonged to a group + if (culturalGroup) { + groups[culturalGroup].push(line); + } + currentExtinf = null; + } + }); + + // Get list of current cultural group files + const currentGroupFiles = Object.keys(groups).map(groupTitle => + `${groupTitle.toLowerCase()}.m3u` + ); + + // Remove obsolete files + existingFiles.forEach(existingFile => { + if (!currentGroupFiles.includes(existingFile)) { + const fileToRemove = path.join(groupsDir, existingFile); + fs.unlinkSync(fileToRemove); + console.log(`Removed obsolete cultural group playlist: ${existingFile}`); + } + }); + + // Write cultural group files + Object.entries(groups).forEach(([groupTitle, groupLines]) => { + const groupFilePath = path.join(groupsDir, `${groupTitle.toLowerCase()}.m3u`); + fs.writeFileSync(groupFilePath, groupLines.join('\n') + '\n'); + console.log(`Created/updated cultural group playlist: ${groupFilePath}`); + }); + + // Generate summary + const summary = Object.entries(groups).map(([group, lines]) => { + const channelCount = (lines.length - 1) / 2; // Subtract header and divide by 2 (EXTINF + URL) + return `${group}: ${channelCount} channels`; + }); + + console.log('\nCultural group split summary:'); + console.log(summary.join('\n')); +} + +const filePath = process.argv[2]; +if (!filePath) { + console.error('Please provide the path to the M3U file'); + process.exit(1); +} + +try { + splitByCulturalGroup(filePath); +} catch (error) { + console.error('Error splitting M3U file:', error.message); + process.exit(1); +} \ No newline at end of file diff --git a/.forgejo/scripts/readme-m3u.js b/.forgejo/scripts/readme-m3u.js index bc60b33..b77547b 100644 --- a/.forgejo/scripts/readme-m3u.js +++ b/.forgejo/scripts/readme-m3u.js @@ -2,9 +2,13 @@ const fs = require('fs'); const path = require('path'); function updateReadme(m3uPath) { - // Get list of group files - const groupsDir = path.join(path.dirname(m3uPath), 'countries'); - const groups = {}; + // Get directories for both types of groups + const countriesDir = path.join(path.dirname(m3uPath), 'countries'); + const culturalDir = path.join(path.dirname(m3uPath), 'cultural-groups'); + const groups = { + countries: {}, + cultural: {} + }; // Read the main M3U to get original group names and channel counts const content = fs.readFileSync(m3uPath, 'utf8'); @@ -16,33 +20,82 @@ function updateReadme(m3uPath) { totalChannels++; const groupMatch = line.match(/group-title="([^"]*)"/); const groupTitle = groupMatch ? groupMatch[1] : 'Unknown'; - groups[groupTitle] = (groups[groupTitle] || 0) + 1; + groups.countries[groupTitle] = (groups.countries[groupTitle] || 0) + 1; } }); - // Start building README content - let readmeContent = '# Mystique IPTV\n\n'; + // Get cultural group counts + if (fs.existsSync(culturalDir)) { + fs.readdirSync(culturalDir) + .filter(file => file.endsWith('.m3u')) + .forEach(file => { + const culturalContent = fs.readFileSync(path.join(culturalDir, file), 'utf8'); + const culturalLines = culturalContent.split('\n'); + const channelCount = culturalLines.filter(line => line.startsWith('#EXTINF')).length; + const groupName = file.replace('.m3u', ''); + groups.cultural[groupName] = channelCount; + }); + } + + // Build README content + let readmeContent = '# Mystique\n\n'; + + // Add description and features + readmeContent += '## About\n\n'; + readmeContent += 'Mystique provides a curated collection of IPTV channels from around the world. '; + readmeContent += 'The channels are organized by both countries and cultural groups for easy access.\n\n'; + + // Add usage section + readmeContent += '## Usage\n\n'; + readmeContent += '1. **Complete Playlist**: Use the main `mystique.m3u` file for access to all channels\n'; + readmeContent += '2. **Country-Specific**: Individual country playlists are available in the `countries/` directory\n'; + readmeContent += '3. **Cultural Groups**: Cultural/linguistic group playlists are available in the `cultural-groups/` directory\n\n'; + + // Add statistics + readmeContent += '## Statistics\n\n'; + readmeContent += `- Total Channels: ${totalChannels}\n`; + readmeContent += `- Countries Available: ${Object.keys(groups.countries).length}\n`; + readmeContent += `- Cultural Groups: ${Object.keys(groups.cultural).length}\n\n`; + + // Add playlists table readmeContent += '## Available Playlists\n\n'; readmeContent += '| Playlist | Channels | Link |\n'; readmeContent += '|----------|-----------|------|\n'; - // Add main playlist first + // Add main playlist const mainPlaylistName = path.basename(m3uPath); - readmeContent += `| **Complete (All countries)** | ${totalChannels} | [${mainPlaylistName}](${mainPlaylistName}) |\n`; + readmeContent += `| **Complete (All channels)** | ${totalChannels} | [${mainPlaylistName}](${mainPlaylistName}) |\n`; - // Sort groups alphabetically - const sortedGroups = Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0])); - - // Add each group to the table - sortedGroups.forEach(([groupName, channelCount]) => { + // Add countries + readmeContent += '| **───────── Countries ─────────** | | |\n'; + const sortedCountryGroups = Object.entries(groups.countries).sort((a, b) => a[0].localeCompare(b[0])); + sortedCountryGroups.forEach(([groupName, channelCount]) => { const safeGroupName = groupName.replace(/[^a-z0-9]/gi, '_').toLowerCase(); readmeContent += `| ${groupName} | ${channelCount} | [${safeGroupName}.m3u](countries/${safeGroupName}.m3u) |\n`; }); + + // Add cultural groups + if (Object.keys(groups.cultural).length > 0) { + readmeContent += '| **───────── Cultural Groups ─────────** | | |\n'; + const sortedCulturalGroups = Object.entries(groups.cultural).sort((a, b) => a[0].localeCompare(b[0])); + sortedCulturalGroups.forEach(([groupName, channelCount]) => { + const displayName = groupName + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + readmeContent += `| ${displayName} | ${channelCount} | [${groupName}.m3u](cultural-groups/${groupName}.m3u) |\n`; + }); + } + + + // Add note about legal usage + readmeContent += '## Legal Notice\n\n'; + readmeContent += 'This playlist is a collection of publicly available IPTV streams. '; + readmeContent += 'Please check your local laws regarding IPTV streaming before using this playlist.\n'; const readmePath = path.join(path.dirname(m3uPath), 'README.md'); - fs.writeFileSync(readmePath, readmeContent); - console.log('README.md has been updated with playlist and group information'); + console.log('README.md has been updated with comprehensive playlist information'); } const filePath = process.argv[2]; diff --git a/.forgejo/workflows/validate.yaml b/.forgejo/workflows/validate.yaml index 4337c9c..115be39 100644 --- a/.forgejo/workflows/validate.yaml +++ b/.forgejo/workflows/validate.yaml @@ -29,9 +29,12 @@ jobs: - name: Run M3U linter run: m3u-linter --config .forgejo/scripts/m3u-linter.config.json mystique.m3u - - name: Split into group playlists + - name: Split into country playlists run: node .forgejo/scripts/split-m3u.js mystique.m3u + - name: Split into cultural group playlists + run: node .forgejo/scripts/cultural-groups.js mystique.m3u + - name: Update README run: node .forgejo/scripts/readme-m3u.js mystique.m3u