add custom fixes

This commit is contained in:
David Claeys 2024-10-04 16:58:16 +02:00
parent f66ad104af
commit 6e16e71ab5
5 changed files with 402 additions and 1 deletions

View File

@ -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

View File

@ -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 : []
}

View File

@ -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)
}
})
}

View File

@ -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`;
}

View File

@ -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