Compare commits

...

15 Commits

Author SHA1 Message Date
2da2e0e4ab edit readme
All checks were successful
Build docker container / build (push) Successful in 7m14s
2024-10-04 17:13:48 +02:00
f55db2a2f2 edit readme 2024-10-04 17:13:17 +02:00
9dbc13ee24 update readme 2024-10-04 17:10:44 +02:00
bf767c9878 update readme 2024-10-04 17:09:49 +02:00
12b1343bd4 update readme 2024-10-04 17:01:13 +02:00
0e70d67740 edit readme 2024-10-04 17:00:06 +02:00
6e16e71ab5 add custom fixes 2024-10-04 16:58:16 +02:00
f66ad104af update readme
All checks were successful
Build docker container / build (push) Successful in 12m47s
2024-10-02 08:33:55 +02:00
df87aec731 update readme 2024-09-30 08:36:15 +02:00
6c8efd06c6 update versions in readme
All checks were successful
Build docker container / build (push) Successful in 10m33s
2024-09-16 08:40:42 +02:00
d197c843ee update versions in readme
All checks were successful
Build docker container / build (push) Successful in 8m18s
2024-09-09 14:15:37 +02:00
745b49d0e8 include versioning
All checks were successful
Build docker container / build (push) Successful in 8m45s
2024-08-26 16:34:46 +02:00
d4bed497a5 update readme 2024-08-05 09:20:04 +02:00
586db42d37 update readme 2024-08-05 09:18:16 +02:00
4cbba7691e update readme 2024-08-05 09:12:50 +02:00
6 changed files with 458 additions and 1 deletions

View File

@ -4,12 +4,16 @@ ARG GIT_BRANCH=master
ENV CRON_SCHEDULE="0 0,12 * * *" ENV CRON_SCHEDULE="0 0,12 * * *"
ENV DAYS=14 ENV DAYS=14
ENV MAX_CONNECTIONS=10 ENV MAX_CONNECTIONS=10
ENV ENABLE_FIXES=false
ARG BIN_FOLDER=/bin ARG BIN_FOLDER=/bin
ARG EPG_FOLDER=epg ARG EPG_FOLDER=epg
ARG FIXES_FOLDER_ARG=fixes
ARG START_SCRIPT_ARG=$BIN_FOLDER/$EPG_FOLDER/start.sh ARG START_SCRIPT_ARG=$BIN_FOLDER/$EPG_FOLDER/start.sh
ENV WORKDIR=${BIN_FOLDER}/${EPG_FOLDER} ENV WORKDIR=${BIN_FOLDER}/${EPG_FOLDER}
ENV FIXES_FOLDER=$FIXES_FOLDER_ARG
ENV START_SCRIPT=$START_SCRIPT_ARG ENV START_SCRIPT=$START_SCRIPT_ARG
COPY channels.xml /config/channels.xml COPY channels.xml /config/channels.xml
ADD $FIXES_FOLDER /fixes
RUN apk update \ RUN apk update \
&& apk upgrade --available \ && apk upgrade --available \
&& apk add curl \ && apk add curl \
@ -40,5 +44,6 @@ COPY serve.json $WORKDIR
RUN chmod +x "$START_SCRIPT" \ RUN chmod +x "$START_SCRIPT" \
&& apk del git curl \ && apk del git curl \
&& rm -rf /var/cache/apk/* && 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 EXPOSE 3000

View File

@ -3,20 +3,40 @@
This repo builds and Docker image of [iptv-org/epg](https://github.com/iptv-org/epg). This repo builds and Docker image of [iptv-org/epg](https://github.com/iptv-org/epg).
The purpose is to make the deployment easier and more suitable for different environments. The purpose is to make the deployment easier and more suitable for different environments.
The original repository of this image is hosted on https://git.claeyscloud.com/david/epg-info-docker.<br>
A public mirror is available at https://github.com/davidclaeysquinones/epg-info-docker.
## Dependencies ## Dependencies
[Node](https://nodejs.org/en)<br> [Node](https://nodejs.org/en)<br>
[pm2](https://www.npmjs.com/package/pm2)<br> [pm2](https://www.npmjs.com/package/pm2)<br>
[serve](https://www.npmjs.com/package/serve)<br> [serve](https://www.npmjs.com/package/serve)<br>
The image is based on `node:21-alpine` in order to be more lightweight.
The `pm2` and `serve` packages are used in order to run the application in the container.
## Docker image ## Docker image
### Paths ### Paths
#### Channels file
An example `channels.xml` is included by default in the image.<br> An example `channels.xml` is included by default in the image.<br>
```xml
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="movistarplus.es" lang="es" xmltv_id="24Horas.es" site_id="24H">24 Horas</channel>
...
</channels>
```
However if you want to configure your own channels you need to provide your own configuration file.<br> However if you want to configure your own channels you need to provide your own configuration file.<br>
You can do this by creating a mapping in the `/config` folder. You can do this by creating a mapping in the `/config` folder.
#### Custom fixes
Through the `ENABLE_FIXES` variable custom provider fixes can be applied to the container.
By default some fixes are available. If you have suggestions or a problem with them please submit an issue.
If for some reason you want to include your own provider fixes this is possible by creation a mapping in the `/fixes` folder.<br>
The expected structure is */fixes/`provider_name`/`provider_name`.config.js*.<br>
It is recommended that you take existing provider code as a base for your customisations.
### Environment Variables ### Environment Variables
| Variable | Description | Default | | Variable | Description | Default |
@ -24,4 +44,40 @@ You can do this by creating a mapping in the `/config` folder.
| CRON_SCHEDULE | CRON expression describing the recurrence for epg retrieval. | `0 0,12 * * *` | | CRON_SCHEDULE | CRON expression describing the recurrence for epg retrieval. | `0 0,12 * * *` |
| DAYS | Describes the desired amount of days in the future for for epg retrieval. | 14 | | DAYS | Describes the desired amount of days in the future for for epg retrieval. | 14 |
| MAX_CONNECTIONS | The maximum amount of parallel connections that can be established | 10 | | MAX_CONNECTIONS | The maximum amount of parallel connections that can be established | 10 |
| ENABLE_FIXES | Some fixes to providers take a long time to be merged into the main branch.<br>When this option is enabled some of these fixes will also be included.<br>The source code for these fixes can be seen under the `fixes` folder.<br> Recreate the container when changing this variable in order for it to take effect | false |
### Compose file
```sh
version: '3.3'
services:
epg:
image: git.claeyscloud.com/david/epg-info:latest
#image: image: git.claeyscloud.com/david/epg-info:latest:latest
volumes:
# add a mapping in order to add the channels file
- /docker/epg:/config
ports:
- 6080:3000
environment:
# specify the time zone for the server
- TZ=Etc/UTC
restart: unless-stopped
```
### Versions
- 1.0.0
[08-01-2024](https://github.com/iptv-org/epg/commit/793c74ca397504fc2afc8fbfa998e0b8e4ca45d9)
- 1.0.1
[08-14-2024](https://github.com/iptv-org/epg/commit/270e85cfae6f0f691c2e6ab7ce511d60fd687565)
- 1.0.2
[09-07-2024](https://github.com/iptv-org/epg/commit/4e3b06a86e225cdd1b9362a683e6770fb68ff28f)
- 1.0.3
[09-14-2024](https://github.com/iptv-org/epg/commit/c69f3c93b1123ddf0fecc62c7067fced59ae4e99)
- 1.0.4
[09-30-2024](https://github.com/iptv-org/epg/commit/d90c7a54b941238cb92391b33d80a75e746d3002)
- 1.0.5
[10-02-2024](https://github.com/iptv-org/epg/commit/713dbf60a1cb9623ffcab6ab370ee9a78b32102b)
- 1.0.6
[10-02-2024](https://github.com/iptv-org/epg/commit/713dbf60a1cb9623ffcab6ab370ee9a78b32102b)<br>Adds possibility to enable custom fixes

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#*=}" ;; work-dir=*) work_dir="${arg#*=}" ;;
days=*) days="${arg#*=}" ;; days=*) days="${arg#*=}" ;;
max_connections=*) max_connections="${arg#*=}" ;; max_connections=*) max_connections="${arg#*=}" ;;
enable_fixes=*) enable_fixes="${arg#*=}" ;;
esac esac
done done
@ -15,6 +16,11 @@ cd $work_dir
echo "working dir : " $(pwd) echo "working dir : " $(pwd)
echo "days : ${days}" echo "days : ${days}"
echo "max_connections : ${max_connections}" 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 pm2 --name epg start npm -- run serve
npm run grab -- --channels=channels.xml --maxConnections=$max_connections --days=$days --gzip npm run grab -- --channels=channels.xml --maxConnections=$max_connections --days=$days --gzip