From 1062769e139ff5f4d5dd707c4739eeb196b850cc Mon Sep 17 00:00:00 2001 From: David Claeys Date: Tue, 27 Oct 2026 10:45:02 +0100 Subject: [PATCH] add fixes to pick.be provider --- fixes/pickx.be/pickx.be.channels.xml | 412 +++++++++++++++++++++++++++ fixes/pickx.be/pickx.be.config.js | 221 +++++++------- 2 files changed, 531 insertions(+), 102 deletions(-) create mode 100644 fixes/pickx.be/pickx.be.channels.xml diff --git a/fixes/pickx.be/pickx.be.channels.xml b/fixes/pickx.be/pickx.be.channels.xml new file mode 100644 index 0000000..1b1ce0f --- /dev/null +++ b/fixes/pickx.be/pickx.be.channels.xml @@ -0,0 +1,412 @@ + + + Maghreb TV + Netflix + National Geographic Wild F + National Geographic Wild N + Das Erste + TIPIK vision + ZDF + Dobbit TV N + VTM 2 HD + Pickx+ F + Pickx+ N + Dobbit TV F + TIPIK HD + Plattelands + Disney Jr. N + Pickx+ F HD + Pickx+ N HD + AB3 HD + Discovery F + Disc HD F + MezzoLive + RTL tvi HD + RTL plug HD + Télé Sambre + Védia + TV Com + TV Lux + MaTélé + Disney+ + BBC First + nick N HD + Play6 + Play6 HD + France 24 HD + Euronews F + VTM 3 HD + Crime District + bel RTL HD + Eclips + ABXPLO HD + Njam! HD + Njam! + arte HD + Animal Pl HD + Ketnet HD + NPO 1 HD + NPO 2 HD + NPO 3 HD + Be1 + Be Ciné + Be Séries + Ciné+OCS + Ciné+Frisson + Ciné+Classic + OUT tv + France 3 HD + France 4 HD + France 5 HD + LN24 + VTM Gold + Penthouse Gold + Pickx+ Sports 1 N + Pickx+ Sports 2 N + Pickx+ Sports 3 N + Pickx+ Sports 4 N + Pickx+ Sports 5 N + Pickx+ Sports 6 N + Pickx+ Sports 8 N HD + Pickx+ Sports 9 N HD + Dusk + Pickx Sports Pop-up N HD + Pickx+ Sports 1 N HD + Pickx+ Sports 2 N HD + Pickx+ Sports 3 N HD + Pickx+ Sports 4 N HD + Pickx+ Sports 5 N HD + Pickx+ Sports 6 N HD + DAZN 1 F + DAZN 1 N + DAZN 2 F + DAZN 2 N + DAZN 1 F HD + DAZN 1 N HD + DAZN 2 F HD + DAZN 2 N HD + Pickx+ Sports 1 F + Pickx+ Sports 3 F + Pickx+ Sports 4 F + Pickx+ Sports 5 F + Pickx+ Sports 6 F + Pickx+ Sports 8 F HD + Pickx+ Sports 9 F HD + Pickx+ Sports 10 F HD + Pickx Sports Pop-up F HD + Pickx+ Sports 1 F HD + Pickx+ Sports 2 F HD + Pickx+ Sports 3 F HD + Pickx+ Sports 4 F HD + Pickx+ Sports 5 F HD + Pickx+ Sports 6 F HD + Pickx+ Sports 10 N HD + StuBru + InfoChannel + Vlaamsparlement.tv + E! HD F + E! HD N + Disn Ch HD F + Disn Ch HD N + 2M Monde + VTM Life HD + Kanaal Z HD + Canal Z HD + TV Breizh HD + TCM Cinéma HD + SyFy HD F + 13è Rue HD + Action HD + Sport 10 + Gulli HD + NGC HD N + Motorvision+ + TV5 Monde HD + Motorvision+ HD + Comedy Central F + nick jr F + VTM 4 HD + Pickx Showcase N HD + Play4 HD + Play5 HD + DAZN Pro League 1 F HD + DAZN Pro League 1 F + TLC + VTM Life + TREK HD + DAZN Pro League 1 N HD + DAZN Pro League 1 N + DAZN Pro League 2 F HD + DAZN Pro League 2 F + DAZN Pro League 2 N HD + DAZN Pro League 2 N + RTL + TMC HD + Play7 HD + MyZen.tv + Museum TV + Antenne Centre HD + ATV HD + AVS HD + BRUZZ HD + bx1 HD + Bouké HD + Canal Zoom HD + Focus TV HD + MaTélé HD + No Télé HD + Ring TV HD + ROBTV HD + Quatre HD + RTV Kempen HD + RTV Mech HD + Télé MB HD + Télé Sambre HD + TV Limburg HD + TV Lux HD + TV Oost HD + Védia HD + WTV HD + Ment TV HD + INPLUS + TF1 Séries Films + Q-Music Kijk Live + BBC Two HD + Eurosp2 HD N + Boom F HD + Cartoonito HD + Cartoon F HD + Cartoon N HD + Inv Disc HD + W Sports + Disney Jr. F HD + Sc et Vie TV HD + Histoire HD + History HD + RAI Uno HD + RAI Due HD + RAI Tre HD + Dobbit N HD + Das Erste HD + Comedy Central N HD + LCI HD + Mangas HD + ZDF HD + Dorcel XXX HD + Passie XXX HD + Pink HD + Tiji HD + Trace HD + Canal J HD + Fashion TV HD + Melody.tv HD + MTV N HD + Al Aoula HD + Al Jazeera HD + AUTOMOTO HD + RFM TV HD + RTL Deutschland HD + TVE International HD + Animaux HD + Euronews E HD + Euronews F HD + HGTV + TV Breizh + Histoire + ContactV + RAI Due + Discovery N + Arte + Action + Cartoon F + Cartoon N + TREK + Disn Ch F + Ushuaïa + RTL plug + TVE Int + TraceUrban + Discovery Science + TV5 Monde + FashionTV + CNN + Kanaal Z + Canal Z + VRT 1 + France 5 + nickelodeon N + VTM + ABXPLORE + RFM TV + VTM 2 + Mangas + NPO 1 + RAI Uno + Animaux + Canal J + AB3 + RTBF TIPIK + RTBF La Une + SyFy F + National Geographic F + Play5 + LCI + TF1 + BBC News + RTL club + BBC One + RTL tvi + France 24 F + bx1 + Eurosport F + NPO 3 + NPO 2 + France 4 + VTM 3 + Play4 + VRT Canvas + Euronews E + National Geographic N + France 2 + France 3 + Eurosp2 F + Animal Planet + Eurosport N + Tiji + TCM Cinéma + Dorcel XXX + Science et Vie TV + Boomerang F + E! F + 13è Rue + AUTOMOTO + Pickx Showcase F + WTV + Focus TV + ATV + TV Limburg + ROBTV + E! N + TV Oost + BRUZZ + RTV Kempen O + RTV Mech + Ring TV + No Télé + Disney Jr. F + Télé MB + Al Aoula + MTV F + MTV N + Comedy Central N + Bouké + Canal Zoom + Antenne C + Passie XXX + nick jr N + History + Melody.tv + Al Jazeera + KTO + nick Toons + TF1 HD + Pink + Ushuaïa HD + Classica + Disc HD N + Sport 10 HD + NGC HD F + VRT 1 HD + RTL club HD + Eurosp HD N + France 2 HD + Pickx+ Sports 7 N + Inv Disc + Pickx+ Sports 9 N + Pickx+ Sports 10 N + Pickx+ Sports 8 N + Pickx+ Sports 10 F + Pickx+ Sports 7 F + Pickx+ Sports 8 F + Pickx+ Sports 9 F + Eurosp2 N + Pickx+ Sports 7 F HD + Pickx+ Sports 7 N HD + BBC Two + VTM HD + VTM 4 + La Trois + Cartoonito + VRT Canvas HD + Ketnet + nick F + Ment TV + La Une HD + Disn Ch N + La Trois HD + Quatre + BRF Fernsehen + BRF 1 + Test VRT ENG + Test 977 + Test RTBF ENG + Test RTL ENG + Test DPG ENG + Test 998 + Horse and Country + Sanity Check Live 01 + Pickx+ F + RTBF La Prem1ère + RTBF VivaCité + RTBF Musiq'3 + RTBF Classic21 + RTBF Tipik Radio + bel RTL + MINT + Contact + Nostalgie + LN Radio + Test 984 + RTL district + Sanity Check Radio + Sanity Check Live 02 + Sanity Check Live 03 + Sanity Check Live 04 + Sanity Check Live 05 + Sanity Check Live 06 + Sanity Check Live 07 + Sanity Check Live 08 + Sanity Check Live 09 + Sanity Check Live 10 + Sanity Check Live 11 + Sanity Check Live 12 + Sanity Check Live 20 + NTVBE Test 11 + NTVBE Test 12 + NTVBE Test 13 + NTVBE Test 14 + NTVBE Test 15 + Mediaset Italia + Studio 100 TV + Proximus Showcase N + Studio 100 TV + STAR Channel + Pickx+ N + Radio 1 + Radio 2 Antwerpen + Radio 2 Limburg + Radio 2 Oost-Vlaanderen + Radio 2 Vlaam-Brabant + Radio 2 West-Vlaanderen + MNM + MNM Hits + Klara + Klara Continuo + Radio Bene + De Tijdloze + Qmusic + JOE + Play Crime + Frame + Pickx Crime + diff --git a/fixes/pickx.be/pickx.be.config.js b/fixes/pickx.be/pickx.be.config.js index 08e45fe..9af8209 100644 --- a/fixes/pickx.be/pickx.be.config.js +++ b/fixes/pickx.be/pickx.be.config.js @@ -1,133 +1,150 @@ -//https://github.com/iptv-org/epg/blob/e4f92bb2a2768dcba3dbbd52b19d78d96bebc31e/sites/pickx.be/pickx.be.config.js - const axios = require('axios') const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') -let apiVersion +dayjs.extend(utc) +dayjs.extend(timezone) + +const API_ENDPOINT = 'https://api.prod-ntvbe.tvapi-web.pickx.be' +const API_IMAGE_ENDPOINT = + 'https://experience-cache.cdi.streaming.proximustv.be/posterserver/poster/EPG' + +const TIMEZONE = 'Europe/Brussels' + +// every tray of the AVS backend is addressed through +// /{tenant}/{version}/{cluster}/{locale}/{platform}/{property}, where the cluster is the access +// level ("A" is anonymous, which is enough for the public guide) and the platform has to be WEB +const BASE_PATH = '/AGL/1.5/A/{locale}/WEB/PXS' + +// the locale only selects the language of the labels the API itself emits, never the language of +// the guide, so a channel keeps its broadcast language whichever locale is requested +const LOCALES = { en: 'ENG', fr: 'FRA', nl: 'NLD', de: 'DEU' } + +const LANGUAGES = { + ara: 'ar', + deu: 'de', + eng: 'en', + fra: 'fr', + ita: 'it', + nld: 'nl', + spa: 'es' +} + +// the channel tray refuses any page wider than this +const CHANNELS_PER_PAGE = 100 module.exports = { site: 'pickx.be', days: 2, - async url({ channel, date }) { - if (!apiVersion) { - await fetchApiVersion() - } - - 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/' + Origin: 'https://tv.pickx.be', + Referer: 'https://tv.pickx.be/' } }, - parser({ channel, content }) { + url({ channel, date }) { + // the guide is cut into Brussels days, while the grabber hands us a UTC moment. Both edges + // are resolved from their own calendar date, because adding a day to the start would add a + // flat 24 hours and so cut the 25 hour day short, or overrun the 23 hour one, when the + // Brussels clock shifts + const start = brusselsMidnight(date) + const stop = brusselsMidnight(date.add(1, 'd')) + + // from/to paginates the channels of the tray, not their programs, so asking for a single + // channel still returns its whole day + return `${API_ENDPOINT}${basePath(channel.lang)}/TRAY/EPG?from=1&to=1&filter_channelIds=${ + channel.site_id + }&filter_startTime=${start.valueOf()}&filter_endTime=${stop.valueOf()}` + }, + parser({ 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.cdi.streaming.proximustv.be/posterserver/poster/EPG/${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(item.programScheduleStart), - stop: dayjs(item.programScheduleEnd) - }) + parseItems(content).forEach(item => { + const data = item.metadata + if (!data) return + + programs.push({ + title: data.title, + sub_title: data.episodeTitle, + description: data.longDescription || data.shortDescription, + category: data.genres, + image: parseImage(data), + season: data.season, + episode: data.episodeNumber, + actors: data.actors, + director: data.directors, + date: data.year, + country: data.country, + start: dayjs(data.airingStartTime), + stop: dayjs(data.airingEndTime) }) - } + }) return programs }, async channels() { - let channels = [] + const channels = [] - const query = { - operationName: 'getChannels', - variables: { - language: 'fr', - 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 - name - language - radio - } - }` - } + for (let from = 1; ; from += CHANNELS_PER_PAGE) { + const data = await fetchChannels(from) + if (!data) break - const data = await axios - .post('https://api.proximusmwc.be/tiams/v3/graphql', query) - .then(r => r.data) - .catch(console.error) + const items = data.containers || [] + items.forEach(item => { + const metadata = item.metadata + if (!metadata) return - data.data.channels.forEach(channel => { - let lang = channel.language || 'fr' - if (channel.language === 'ger') lang = 'de' - - channels.push({ - lang, - site_id: channel.id, - name: channel.name + channels.push({ + lang: LANGUAGES[metadata.channelLanguage] || 'fr', + site_id: String(metadata.channelId), + name: metadata.channelName + }) }) - }) + + if (!items.length || channels.length >= data.total) break + } return channels } } -async function fetchApiVersion() { - const hashUrl = 'https://www.pickx.be/nl/televisie/tv-gids' - const hashData = await axios - .get(hashUrl) - .then(r => { - const re = /"hashes":\["(.*)"\]/ - const match = r.data.match(re) - if (match && match[1]) { - return match[1] - } else { - throw new Error('React app version hash not found') +function basePath(lang) { + return BASE_PATH.replace('{locale}', LOCALES[lang] || LOCALES.nl) +} + +function brusselsMidnight(date) { + return dayjs.tz(date.format('YYYY-MM-DD'), TIMEZONE).startOf('d') +} + +function fetchChannels(from) { + return axios + .get(`${API_ENDPOINT}${basePath('nl')}/TRAY/LIVECHANNELS`, { + params: { from, to: from + CHANNELS_PER_PAGE - 1 }, + headers: { + Origin: 'https://tv.pickx.be', + Referer: 'https://tv.pickx.be/' } }) + .then(r => r.data.resultObj) .catch(console.error) +} - const versionUrl = `https://www.pickx.be/api/s-${hashData}` - const response = await axios.get(versionUrl, { - headers: { - Origin: 'https://www.pickx.be', - Referer: 'https://www.pickx.be/' - } - }) +function parseImage(data) { + const filename = data.pictureUrl || data.extendedMetadata?.images?.landscapePoster - return new Promise((resolve, reject) => { - try { - 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 during fetchApiVersion:', error) - reject(error) - } - }) -} \ No newline at end of file + return filename ? `${API_IMAGE_ENDPOINT}/${filename}` : null +} + +function parseItems(content) { + if (!content) return [] + + try { + const data = JSON.parse(content) + // the tray nests the programs of each requested channel under their own container + const channel = data?.resultObj?.containers?.[0] + + return channel?.containers || [] + } catch { + return [] + } +}