diff --git a/Dockerfile b/Dockerfile index 8ac12ac..772c527 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,16 @@ ARG GIT_BRANCH=master ENV CRON_SCHEDULE="0 0,12 * * *" ENV DAYS=14 ENV MAX_CONNECTIONS=10 +ENV ENABLE_FIXES=false ARG BIN_FOLDER=/bin ARG EPG_FOLDER=epg +ARG FIXES_FOLDER_ARG=fixes ARG START_SCRIPT_ARG=$BIN_FOLDER/$EPG_FOLDER/start.sh ENV WORKDIR=${BIN_FOLDER}/${EPG_FOLDER} +ENV FIXES_FOLDER=$FIXES_FOLDER_ARG ENV START_SCRIPT=$START_SCRIPT_ARG COPY channels.xml /config/channels.xml +ADD $FIXES_FOLDER /fixes RUN apk update \ && apk upgrade --available \ && apk add curl \ @@ -40,5 +44,6 @@ COPY serve.json $WORKDIR RUN chmod +x "$START_SCRIPT" \ && apk del git curl \ && rm -rf /var/cache/apk/* -ENTRYPOINT bash $START_SCRIPT chron-schedule="$CRON_SCHEDULE" work-dir="$WORKDIR" days="$DAYS" max_connections="$MAX_CONNECTIONS" +SHELL ["/bin/bash", "-c"] +ENTRYPOINT bash $START_SCRIPT chron-schedule="$CRON_SCHEDULE" work-dir="$WORKDIR" days="$DAYS" max_connections="$MAX_CONNECTIONS" enable_fixes="$ENABLE_FIXES" EXPOSE 3000 \ No newline at end of file diff --git a/fixes/movistarplus.es/movistarplus.es.config.js b/fixes/movistarplus.es/movistarplus.es.config.js new file mode 100644 index 0000000..29ddc9e --- /dev/null +++ b/fixes/movistarplus.es/movistarplus.es.config.js @@ -0,0 +1,74 @@ +const { DateTime } = require('luxon') + +const API_PROD_ENDPOINT = 'https://www.movistarplus.es/programacion-tv' +const API_IMAGE_ENDPOINT = 'https://www.movistarplus.es/recorte/n/caratulaH/'; + +module.exports = { + site: 'movistarplus.es', + days: 2, + url: function ({ date }) { + return `${API_PROD_ENDPOINT}/${date.format('YYYY-MM-DD')}?v=json` + }, + parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + let guideDate = date + + items.forEach(item => { + let startTime = DateTime.fromFormat( + `${guideDate.format('YYYY-MM-DD')} ${item.HORA_INICIO}`, + 'yyyy-MM-dd HH:mm', + { + zone: 'Europe/Madrid' + } + ).toUTC() + let stopTime = DateTime.fromFormat( + `${guideDate.format('YYYY-MM-DD')} ${item.HORA_FIN}`, + 'yyyy-MM-dd HH:mm', + { + zone: 'Europe/Madrid' + } + ).toUTC() + if (stopTime < startTime) { + guideDate = guideDate.add(1, 'd') + stopTime = stopTime.plus({ days: 1 }) + } + programs.push({ + title: item.TITULO, + icon: parseIcon(item, channel), + category: item.GENERO, + start: startTime, + stop: stopTime + }) + }) + return programs + }, + async channels() { + const axios = require('axios') + const dayjs = require('dayjs') + const data = await axios + .get(`${API_PROD_ENDPOINT}/${dayjs().format('YYYY-MM-DD')}?v=json`) + .then(r => r.data) + .catch(console.log) + + return Object.values(data.data).map(item => { + return { + lang: 'es', + site_id: item.DATOS_CADENA.CODIGO, + name: item.DATOS_CADENA.NOMBRE + } + }) + } +} + +function parseIcon(item, channel) { + return `${API_IMAGE_ENDPOINT}/M${channel.site_id}P${item.ELEMENTO}`; +} + +function parseItems(content, channel) { + const json = typeof content === 'string' ? JSON.parse(content) : content + if (!(`${channel.site_id}-CODE` in json.data)) return [] + const data = json.data[`${channel.site_id}-CODE`] + return data ? data.PROGRAMAS : [] +} \ No newline at end of file diff --git a/fixes/pickx.be/pickx.be.config.js b/fixes/pickx.be/pickx.be.config.js new file mode 100644 index 0000000..3a39c2e --- /dev/null +++ b/fixes/pickx.be/pickx.be.config.js @@ -0,0 +1,178 @@ +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +let apiVersion +let isApiVersionFetched = false + +;(async () => { + try { + await fetchApiVersion() + isApiVersionFetched = true + } catch (error) { + console.error('Error during script initialization:', error) + } +})() + +dayjs.extend(utc) + +module.exports = { + site: 'pickx.be', + days: 2, + apiVersion: function () { + return apiVersion + }, + fetchApiVersion: fetchApiVersion, // Export fetchApiVersion + url: async function ({ channel, date }) { + while (!isApiVersionFetched) { + await new Promise(resolve => setTimeout(resolve, 100)) // Wait for 100 milliseconds + } + return `https://px-epg.azureedge.net/airings/${apiVersion}/${date.format( + 'YYYY-MM-DD' + )}/channel/${channel.site_id}?timezone=Europe%2FBrussels` + }, + request: { + headers: { + Origin: 'https://www.pickx.be', + Referer: 'https://www.pickx.be/' + } + }, + parser({ channel, content }) { + const programs = [] + if (content) { + const items = JSON.parse(content) + items.forEach(item => { + programs.push({ + title: item.program.title, + sub_title: item.program.episodeTitle, + description: item.program.description, + category: item.program.translatedCategory?.[channel.lang] + ? item.program.translatedCategory[channel.lang] + : item.program.category.split('.')[1], + image: item.program.posterFileName + ? `https://experience-cache.proximustv.be/posterserver/poster/EPG/w-166_h-110/${item.program.posterFileName}` + : null, + season: item.program.seasonNumber, + episode: item.program.episodeNumber, + actors: item.program.actors, + director: item.program.director ? [item.program.director] : null, + start: dayjs.utc(item.programScheduleStart), + stop: dayjs.utc(item.programScheduleEnd) + }) + }) + } + + return programs + }, + async channels({ lang = '' }) { + const query = { + operationName: 'getChannels', + variables: { + language: lang, + queryParams: {}, + id: '0', + params: { + shouldReadFromCache: true + } + }, + query: `query getChannels($language: String!, $queryParams: ChannelQueryParams, $id: String, $params: ChannelParams) { + channels(language: $language, queryParams: $queryParams, id: $id, params: $params) { + id + channelReferenceNumber + name + callLetter + number + logo { + key + url + __typename + } + language + hd + radio + replayable + ottReplayable + playable + ottPlayable + recordable + subscribed + cloudRecordable + catchUpWindowInHours + isOttNPVREnabled + ottNPVRStart + subscription { + channelRef + subscribed + upselling { + upsellable + packages + __typename + } + __typename + } + packages + __typename + } + }` + } + const result = await axios + .post('https://api.proximusmwc.be/tiams/v2/graphql', query) + .then(r => r.data) + .catch(console.error) + + return ( + result?.data?.channels + .filter( + channel => + !channel.radio && (!lang || channel.language === (lang === 'de' ? 'ger' : lang)) + ) + .map(channel => { + return { + lang: channel.language === 'ger' ? 'de' : channel.language, + site_id: channel.id, + name: channel.name + } + }) || [] + ) + } +} +function fetchApiVersion() { + return new Promise(async (resolve, reject) => { + try { + // https://px-epg.azureedge.net/version is deprecated + // probably the version url will be changed around over time + + //history of used version urls + //const versionUrl = 'https://www.pickx.be/api/s-3b36540f3cef64510112f3f95c2c0cdca321997ed2b1042ad778523235e155eb' + //const versionUrl = 'https://www.pickx.be/api/s-671f172425e1bc74cd0440fd67aaa6cbe68b582f3f401186c2f46ae97e80516b' + //const versionUrl = 'https://www.pickx.be/api/s-a6b4b4fefaa20e438523a6167e63b8504d96b9df8303473349763c4418cffe30' + //const versionUrl = 'https://www.pickx.be/api/s-8546c5fd136241d42aab714d2fe3ccc5671fd899035efae07cd0b8f4eb23994e' + //const versionUrl = 'https://www.pickx.be/api/s-64464ad9a3bc117af5dca620027216ecade6a51c230135a0f134c0ee042ff407'; + //const versionUrl = 'https://www.pickx.be/api/s-626d8fdabfb1d44e5a614cd69f4b45d6843fdb63566fc80ea4f97f40e4ea3152'; + //const versionUrl = 'https://www.pickx.be/api/s-cefaf96e249e53648c4895c279e7a621233c50b4357d62b0bdf6bff45f31b5c0'; + //const versionUrl = 'https://www.pickx.be/api/s-7fa35253080e9665f9c7d9d85e707d6fb1d1bf07ede11965e859fcb57c723949'; + //the new strategy to break the provider is to leave old version url's available and to return invalid results on those endpoints + + const versionUrl = 'https://www.pickx.be/api/s-0e58be3938175b6b900dfb5233bd5cfc0bcf915b633fe57b935f7ce8dbe5f6eb'; + + + const response = await axios.get(versionUrl, { + headers: { + Origin: 'https://www.pickx.be', + Referer: 'https://www.pickx.be/' + } + }) + + if (response.status === 200) { + apiVersion = response.data.version + resolve() + } else { + console.error(`Failed to fetch API version. Status: ${response.status}`) + reject(`Failed to fetch API version. Status: ${response.status}`) + } + } catch (error) { + console.error('Error fetching API version:', error.message) + reject(error) + } + }) +} \ No newline at end of file diff --git a/fixes/telenet.tv/telenet.tv.config.js b/fixes/telenet.tv/telenet.tv.config.js new file mode 100644 index 0000000..cbae3c7 --- /dev/null +++ b/fixes/telenet.tv/telenet.tv.config.js @@ -0,0 +1,138 @@ +const axios = require('axios') +const dayjs = require('dayjs') + +const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' +const API_PROD_ENDPOINT = 'https://spark-prod-be.gnp.cloud.telenet.tv/eng/web/linear-service/v2' +const API_IMAGE_ENDPOINT = 'https://staticqbr-prod-be.gnp.cloud.telenet.tv/image-service'; + +module.exports = { + site: 'telenet.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(6, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(12, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(18, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item, channel) + programs.push({ + title: item.title, + icon: parseIcon(item), + description: detail.longDescription, + category: detail.genres, + actors: detail.actors, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_PROD_ENDPOINT}/channels?cityId=28001&language=en&productClass=Orion-DASH`) + .then(r => r.data) + .catch(console.log) + + return data.map(item => { + return { + lang: 'nl', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item, channel) { + if (!item.id) return {} + const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} + +function parseIcon(item) { + return `${API_IMAGE_ENDPOINT}/intent/${item.id}/posterTile`; +} \ No newline at end of file diff --git a/start.sh b/start.sh index 831aa5d..cba32f9 100644 --- a/start.sh +++ b/start.sh @@ -7,6 +7,7 @@ for arg in "$@"; do work-dir=*) work_dir="${arg#*=}" ;; days=*) days="${arg#*=}" ;; max_connections=*) max_connections="${arg#*=}" ;; + enable_fixes=*) enable_fixes="${arg#*=}" ;; esac done @@ -15,6 +16,11 @@ cd $work_dir echo "working dir : " $(pwd) echo "days : ${days}" echo "max_connections : ${max_connections}" +echo "enable_fixes : ${enable_fixes}" + +if [ "$enable_fixes" = true ] ; then + cp -R /fixes/* /bin/epg/sites/ +fi pm2 --name epg start npm -- run serve npm run grab -- --channels=channels.xml --maxConnections=$max_connections --days=$days --gzip