Update format.js
- Adds stream quality parser - More detailed console logs when debug mode is enabled
This commit is contained in:
		| @@ -1,173 +1,231 @@ | |||||||
| const helper = require('./helper') | const helper = require('./helper') | ||||||
|  | const axios = require('axios') | ||||||
|  | const instance = axios.create({ timeout: 1000, maxContentLength: 1000 }) | ||||||
|  |  | ||||||
| const config = { | const config = { | ||||||
|   debug: process.env.npm_config_debug || false, |   debug: process.env.npm_config_debug || false, | ||||||
|   country: process.env.npm_config_country, |   country: process.env.npm_config_country, | ||||||
|   exclude: process.env.npm_config_exclude, |   exclude: process.env.npm_config_exclude, | ||||||
|   epg: process.env.npm_config_epg || false |   epg: process.env.npm_config_epg || false, | ||||||
|  |   resolution: process.env.npm_config_resolution || false | ||||||
| } | } | ||||||
|  |  | ||||||
| let updated = 0 | let globalBuffer = [] | ||||||
| let items = [] |  | ||||||
|  |  | ||||||
| async function main() { | async function main() { | ||||||
|   console.log(`Parsing index...`) |  | ||||||
|   const index = parseIndex() |   const index = parseIndex() | ||||||
|  |  | ||||||
|   for (let item of index.items) { |   for (const item of index.items) { | ||||||
|     if (item.name === 'Unsorted') continue |     await loadPlaylist(item.url) | ||||||
|  |       .then(addToBuffer) | ||||||
|     console.log(`Processing '${item.url}'...`) |       .then(sortChannels) | ||||||
|     let playlist = parsePlaylist(item.url) |       .then(removeDuplicates) | ||||||
|     items = items.concat(playlist.items) |       .then(detectResolution) | ||||||
|  |       .then(updateFromEPG) | ||||||
|     if (config.debug) { |       .then(updatePlaylist) | ||||||
|       console.log(`Sorting channels...`) |       .then(done) | ||||||
|     } |  | ||||||
|     playlist = sortChannels(playlist) |  | ||||||
|  |  | ||||||
|     if (config.debug) { |  | ||||||
|       console.log(`Removing duplicates...`) |  | ||||||
|     } |  | ||||||
|     playlist = removeDuplicates(playlist) |  | ||||||
|  |  | ||||||
|     if (config.epg) { |  | ||||||
|       const tvgUrl = playlist.header.attrs['x-tvg-url'] |  | ||||||
|       if (tvgUrl) { |  | ||||||
|         if (config.debug) { |  | ||||||
|           console.log(`Loading EPG from '${tvgUrl}'...`) |  | ||||||
|         } |  | ||||||
|         const epg = await loadEPG(tvgUrl) |  | ||||||
|         if (config.debug) { |  | ||||||
|           console.log(`Adding the missing data from EPG...`) |  | ||||||
|         } |  | ||||||
|         playlist = addDataFromEPG(playlist, epg) |  | ||||||
|       } else { |  | ||||||
|         if (config.debug) { |  | ||||||
|           console.log(`EPG source is not found`) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     updatePlaylist(item.url, playlist) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   console.log(`Processing 'channels/unsorted.m3u'...`) |   if (index.items.length) { | ||||||
|   filterUnsorted() |     await loadPlaylist('channels/unsorted.m3u').then(removeUnsortedDuplicates).then(updatePlaylist) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   console.log('Done.\n') |   finish() | ||||||
| } | } | ||||||
|  |  | ||||||
| function parseIndex() { | function parseIndex() { | ||||||
|  |   console.info(`Parsing 'index.m3u'...`) | ||||||
|   const playlist = helper.parsePlaylist('index.m3u') |   const playlist = helper.parsePlaylist('index.m3u') | ||||||
|   playlist.items = helper.filterPlaylists(playlist.items, config.country, config.exclude) |   playlist.items = helper | ||||||
|  |     .filterPlaylists(playlist.items, config.country, config.exclude) | ||||||
|   console.log(`Found ${playlist.items.length + 1} playlist(s)`) |     .filter(i => i.url !== 'channels/unsorted.m3u') | ||||||
|  |   console.info(`Found ${playlist.items.length} playlist(s)`) | ||||||
|  |   console.info(`\n------------------------------------------\n`) | ||||||
|  |  | ||||||
|   return playlist |   return playlist | ||||||
| } | } | ||||||
|  |  | ||||||
| function parsePlaylist(url) { | async function loadPlaylist(url) { | ||||||
|  |   console.info(`Processing '${url}'...`) | ||||||
|   const playlist = helper.parsePlaylist(url) |   const playlist = helper.parsePlaylist(url) | ||||||
|  |   playlist.url = url | ||||||
|   playlist.items = playlist.items.map(item => { |   playlist.changed = false | ||||||
|     return helper.createChannel(item) |   playlist.items = playlist.items | ||||||
|   }) |     .map(item => { | ||||||
|  |       return helper.createChannel(item) | ||||||
|  |     }) | ||||||
|  |     .filter(i => i.url) | ||||||
|  |  | ||||||
|   return playlist |   return playlist | ||||||
| } | } | ||||||
|  |  | ||||||
| function sortChannels(playlist) { | async function addToBuffer(playlist) { | ||||||
|   const channels = JSON.stringify(playlist.items) |   if (playlist.url === 'channels/unsorted.m3u') return playlist | ||||||
|  |   globalBuffer = globalBuffer.concat(playlist.items) | ||||||
|  |  | ||||||
|  |   return playlist | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function sortChannels(playlist) { | ||||||
|  |   if (config.debug) console.info(`  Sorting channels...`) | ||||||
|   playlist.items = helper.sortBy(playlist.items, ['name', 'url']) |   playlist.items = helper.sortBy(playlist.items, ['name', 'url']) | ||||||
|  |   if (config.debug) console.info(`    Channels sorted by name.`) | ||||||
|  |  | ||||||
|   return playlist |   return playlist | ||||||
| } | } | ||||||
|  |  | ||||||
| function removeDuplicates(playlist) { | async function removeDuplicates(playlist) { | ||||||
|  |   if (config.debug) console.info(`  Looking for duplicates...`) | ||||||
|   let buffer = {} |   let buffer = {} | ||||||
|   const channels = JSON.stringify(playlist.items) |   const items = playlist.items.filter(i => { | ||||||
|   playlist.items = playlist.items.filter(i => { |     const result = typeof buffer[i.url] === 'undefined' | ||||||
|     let result = typeof buffer[i.url] === 'undefined' |  | ||||||
|  |  | ||||||
|     if (result) { |     if (result) { | ||||||
|       buffer[i.url] = true |       buffer[i.url] = true | ||||||
|     } else { |     } else if (config.debug) { | ||||||
|       if (config.debug) { |       console.info(`    '${i.url}' removed`) | ||||||
|         console.log(`Duplicate of '${i.name}' has been removed`) |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return result |     return result | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   return playlist |   if (config.debug && items.length === playlist.items.length) { | ||||||
| } |     console.info(`    No duplicates were found.`) | ||||||
|  |  | ||||||
| async function loadEPG(url) { |  | ||||||
|   try { |  | ||||||
|     return await helper.parseEPG(url) |  | ||||||
|   } catch (err) { |  | ||||||
|     console.error(`Error: could not load '${url}'`) |  | ||||||
|     return |  | ||||||
|   } |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| function addDataFromEPG(playlist, epg) { |   playlist.items = items | ||||||
|   if (!epg) return playlist |  | ||||||
|  |  | ||||||
|   for (let channel of playlist.items) { |  | ||||||
|     if (!channel.tvg.id) continue |  | ||||||
|  |  | ||||||
|     const epgItem = epg.channels[channel.tvg.id] |  | ||||||
|  |  | ||||||
|     if (!epgItem) continue |  | ||||||
|  |  | ||||||
|     if (!channel.tvg.name && epgItem.name.length) { |  | ||||||
|       channel.tvg.name = epgItem.name[0].value |  | ||||||
|       playlist.changed = true |  | ||||||
|       if (config.debug) { |  | ||||||
|         console.log(`Added tvg-name '${channel.tvg.name}' to '${channel.name}'`) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!channel.language.length && epgItem.name.length && epgItem.name[0].lang) { |  | ||||||
|       channel.setLanguage(epgItem.name[0].lang) |  | ||||||
|       playlist.changed = true |  | ||||||
|       if (config.debug) { |  | ||||||
|         console.log(`Added tvg-language '${epgItem.name[0].lang}' to '${channel.name}'`) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!channel.logo && epgItem.icon.length) { |  | ||||||
|       channel.logo = epgItem.icon[0] |  | ||||||
|       playlist.changed = true |  | ||||||
|       if (config.debug) { |  | ||||||
|         console.log(`Added tvg-logo '${channel.logo}' to '${channel.name}'`) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return playlist |   return playlist | ||||||
| } | } | ||||||
|  |  | ||||||
| function updatePlaylist(filepath, playlist) { | async function detectResolution(playlist) { | ||||||
|   helper.createFile(filepath, playlist.getHeader()) |   if (!config.resolution) return playlist | ||||||
|   for (let channel of playlist.items) { |   if (config.debug) console.info(`  Detecting resolution...`) | ||||||
|     helper.appendToFile(filepath, channel.toShortString()) |   const results = [] | ||||||
|  |   for (const item of playlist.items) { | ||||||
|  |     const url = item.url | ||||||
|  |     if (config.debug) console.info(`    Fetching '${url}'...`) | ||||||
|  |     const response = await instance.get(url).catch(err => {}) | ||||||
|  |     if (isValid(response)) { | ||||||
|  |       const resolution = parseResolution(response.data) | ||||||
|  |       if (resolution) { | ||||||
|  |         item.resolution = resolution | ||||||
|  |       } | ||||||
|  |       if (config.debug) console.info(`      Output: ${JSON.stringify(resolution)}`) | ||||||
|  |     } else { | ||||||
|  |       if (config.debug) console.error(`      Error: invalid response`) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     results.push(item) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   playlist.items = results | ||||||
|  |  | ||||||
|  |   return playlist | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateFromEPG(playlist) { | ||||||
|  |   if (!config.epg) return playlist | ||||||
|  |   const tvgUrl = playlist.header.attrs['x-tvg-url'] | ||||||
|  |   if (!tvgUrl) return playlist | ||||||
|  |   if (config.debug) console.info(`  Loading EPG from '${tvgUrl}'...`) | ||||||
|  |  | ||||||
|  |   return helper | ||||||
|  |     .parseEPG(tvgUrl) | ||||||
|  |     .then(epg => { | ||||||
|  |       if (!epg) return playlist | ||||||
|  |  | ||||||
|  |       playlist.items.map(channel => { | ||||||
|  |         if (!channel.tvg.id) return channel | ||||||
|  |         const epgItem = epg.channels[channel.tvg.id] | ||||||
|  |         if (!epgItem) return channel | ||||||
|  |         if (!channel.tvg.name && epgItem.name.length) { | ||||||
|  |           channel.tvg.name = epgItem.name[0].value | ||||||
|  |           playlist.changed = true | ||||||
|  |           if (config.debug) { | ||||||
|  |             console.info(`    Added tvg-name '${channel.tvg.name}' to '${channel.name}'`) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (!channel.language.length && epgItem.name.length && epgItem.name[0].lang) { | ||||||
|  |           channel.setLanguage(epgItem.name[0].lang) | ||||||
|  |           playlist.changed = true | ||||||
|  |           if (config.debug) { | ||||||
|  |             console.info(`    Added tvg-language '${epgItem.name[0].lang}' to '${channel.name}'`) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (!channel.logo && epgItem.icon.length) { | ||||||
|  |           channel.logo = epgItem.icon[0] | ||||||
|  |           playlist.changed = true | ||||||
|  |           if (config.debug) { | ||||||
|  |             console.info(`    Added tvg-logo '${channel.logo}' to '${channel.name}'`) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |       return playlist | ||||||
|  |     }) | ||||||
|  |     .catch(err => { | ||||||
|  |       if (config.debug) console.log(`    Error: EPG could not be loaded`) | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function parseResolution(string) { | ||||||
|  |   const regex = /RESOLUTION=(\d+)x(\d+)/gm | ||||||
|  |   const match = string.matchAll(regex) | ||||||
|  |   const arr = Array.from(match).map(m => ({ | ||||||
|  |     width: parseInt(m[1]), | ||||||
|  |     height: parseInt(m[2]) | ||||||
|  |   })) | ||||||
|  |  | ||||||
|  |   return arr.length | ||||||
|  |     ? arr.reduce(function (prev, current) { | ||||||
|  |         return prev.height > current.height ? prev : current | ||||||
|  |       }) | ||||||
|  |     : undefined | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function removeUnsortedDuplicates(playlist) { | ||||||
|  |   if (config.debug) console.info(`  Looking for duplicates...`) | ||||||
|  |   const urls = globalBuffer.map(i => i.url) | ||||||
|  |   const items = playlist.items.filter(i => !urls.includes(i.url)) | ||||||
|  |   if (items.length === playlist.items.length) { | ||||||
|  |     if (config.debug) console.info(`    No duplicates were found.`) | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  |   playlist.items = items | ||||||
|  |  | ||||||
|  |   return playlist | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function sleep(ms) { | ||||||
|  |   return function (x) { | ||||||
|  |     return new Promise(resolve => setTimeout(() => resolve(x), ms)) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function filterUnsorted() { | function isValid(response) { | ||||||
|   const urls = items.map(i => i.url) |   return response && response.status === 200 && /^#EXTM3U/.test(response.data) | ||||||
|   const unsortedPlaylist = parsePlaylist('channels/unsorted.m3u') | } | ||||||
|   const before = unsortedPlaylist.items.length |  | ||||||
|   unsortedPlaylist.items = unsortedPlaylist.items.filter(i => !urls.includes(i.url)) |  | ||||||
|  |  | ||||||
|   if (before !== unsortedPlaylist.items.length) { | async function updatePlaylist(playlist) { | ||||||
|     updatePlaylist('channels/unsorted.m3u', unsortedPlaylist) |   if (!playlist) { | ||||||
|     updated++ |     console.info(`No changes have been made.`) | ||||||
|  |     return false | ||||||
|   } |   } | ||||||
|  |   helper.createFile(playlist.url, playlist.getHeader()) | ||||||
|  |   for (let channel of playlist.items) { | ||||||
|  |     helper.appendToFile(playlist.url, channel.toShortString()) | ||||||
|  |   } | ||||||
|  |   console.info(`File has been updated.`) | ||||||
|  |  | ||||||
|  |   return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function done() { | ||||||
|  |   if (config.debug) console.info(` `) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function finish() { | ||||||
|  |   console.info(`\n------------------------------------------\n`) | ||||||
|  |   console.info('Done.\n') | ||||||
| } | } | ||||||
|  |  | ||||||
| main() | main() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user