diff --git a/fixes/movistarplus.es/movistarplus.es.config.js b/fixes/movistarplus.es/movistarplus.es.config.js index 930b40e..22b91ac 100644 --- a/fixes/movistarplus.es/movistarplus.es.config.js +++ b/fixes/movistarplus.es/movistarplus.es.config.js @@ -1,19 +1,18 @@ //https://github.com/iptv-org/epg/blob/master/sites/movistarplus.es/movistarplus.es.config.js const axios = require('axios') -const cheerio = require('cheerio') const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') const timezone = require('dayjs/plugin/timezone') - +const utc = require('dayjs/plugin/utc') dayjs.extend(utc) dayjs.extend(timezone) +dayjs.tz.setDefault('Europe/Madrid') module.exports = { site: 'movistarplus.es', days: 2, url({ channel, date }) { - return `https://www.movistarplus.es/programacion-tv/${channel.site_id}/${date.format('YYYY-MM-DD')}` + return `https://ottcache.dof6.com/movistarplus/webplayer/OTT/epg?from=${date.format('YYYY-MM-DDTHH:mm:ss')}&span=1&channel=${channel.site_id}&version=8&mdrm=true&tlsstream=true&demarcation=18` }, request: { headers: { @@ -25,122 +24,97 @@ module.exports = { }, maxRedirects: 5 }, - async parser({ content, date }) { + async parser({ content }) { let programs = [] - const $ = cheerio.load(content) - - const programDivs = $('div[id^="ele-"]').toArray() - - for (let i = 0; i < programDivs.length; i++) { - const el = $(programDivs[i]) - - const title = el.find('li.title').text().trim() - if (!title) continue - - const timeText = el.find('li.time').text().trim() - if (!timeText) continue - - const [hours, minutes] = timeText.split(':').map(h => parseInt(h, 10)) - - // Parse time in Spain timezone (Europe/Madrid) - let startDate = dayjs.tz( - `${date.format('YYYY-MM-DD')} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`, - 'YYYY-MM-DD HH:mm', - 'Europe/Madrid' - ) - - // If the time is in early morning (before 5 AM), it's the next day - if (hours < 5) { - startDate = startDate.add(1, 'day') - } - - // Calculate end time from next program's start time - let endDate - if (i < programDivs.length - 1) { - const nextEl = $(programDivs[i + 1]) - const nextTimeText = nextEl.find('li.time').text().trim() - if (nextTimeText) { - const [nextHours, nextMinutes] = nextTimeText.split(':').map(h => parseInt(h, 10)) - endDate = dayjs.tz( - `${date.format('YYYY-MM-DD')} ${nextHours.toString().padStart(2, '0')}:${nextMinutes.toString().padStart(2, '0')}`, - 'YYYY-MM-DD HH:mm', - 'Europe/Madrid' - ) - - // If the next time is in early morning (before 5 AM), it's the next day - if (nextHours < 5) { - endDate = endDate.add(1, 'day') - } - - // If end time is still before or same as start time, add another day - if (endDate.isBefore(startDate) || endDate.isSame(startDate)) { - endDate = endDate.add(1, 'day') - } - } - } - - // If no end time, use start of next day - if (!endDate) { - endDate = startDate.add(1, 'day').startOf('day') - } - - const programLink = el.find('a').attr('href') - let description = null - - if (programLink) { - description = await getProgramDescription(programLink).catch(() => null) - } + let items = await parseItems(content) + if (!items.length) return programs + items.forEach(el => { programs.push({ - title, - description, - start: startDate, - stop: endDate + title: el.title, + description: el.description, + icon: el.icon, + images: el.images, + season: el.season, + episode: el.episode, + start: el.start, + stop: el.stop }) - } - + }) return programs }, async channels() { - const html = await axios - .get('https://www.movistarplus.es/programacion-tv', { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' - } - }) + const json = await axios + .get( + 'https://ottcache.dof6.com/movistarplus/webplayer/OTT/contents/channels?mdrm=true&tlsstream=true&demarcation=18&version=8' + ) .then(r => r.data) .catch(console.log) - const $ = cheerio.load(html) - let scheme = $('script:contains(ItemList)').html() - scheme = JSON.parse(scheme) - - return scheme.itemListElement.map(el => { - const urlParts = el.item.url.split('/') - const site_id = urlParts.pop().toLowerCase() - + // Load JSON, CodCadenaTv is the closest to the old MVSTR site ch. ID + return json.map(channel => { return { lang: 'es', - name: el.item.name, - site_id + site_id: channel.CodCadenaTv, + name: channel.Nombre, + logo: channel.Logo || channel.Logos?.[0]?.uri || null } }) } } -async function getProgramDescription(programUrl) { - const response = await axios.get(programUrl, { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - Referer: 'https://www.movistarplus.es/programacion-tv/' - } - }) +function parseImages(images) { + return images.filter(image => image.id === 'watch2tgr-end').map(image => image.uri) +} - const $ = cheerio.load(response.data) - const description = $('.show-content .text p').first().text().trim() || null - - return description +async function parseItems(content) { + try { + const data = JSON.parse(content) + const programs = Array.isArray(data) ? data : [data] + return await Promise.all( + programs.map(async json => { + const start = dayjs.utc(Number(json?.FechaHoraInicio)) + const stop = dayjs.utc(Number(json?.FechaHoraFin)) + const ficha = json?.Ficha || null + if (!ficha) { + return { + title: json?.Titulo || '', + description: json?.Resena || '', + icon: json?.Imagen || '', + images: json.Imagenes ? parseImages(json.Imagenes) : [], + start, + stop + } + } else { + try { + const fichaJson = await axios.get(ficha).then(r => r.data) + return { + title: json?.Titulo || fichaJson?.Titulo || '', + description: fichaJson?.Descripcion || json?.Resena || '', + icon: fichaJson?.Imagen || '', + images: fichaJson.Imagenes ? parseImages(fichaJson.Imagenes) : [], + actors: fichaJson?.Actores || [], + directors: fichaJson?.Directores || [], + classification: fichaJson?.Clasificacion || '', + season: fichaJson?.Temporada || null, + episode: fichaJson?.NumeroEpisodio || null, + start, + stop + } + } catch { + return { + title: json?.Titulo || '', + description: json?.Resena || '', + icon: json?.Imagen || '', + images: json.Imagenes ? parseImages(json.Imagenes) : [], + start, + stop + } + } + } + }) + ) + } catch { + return [] + } } \ No newline at end of file diff --git a/fixes/orangetv.orange.es/orangetv.orange.es.config.js b/fixes/orangetv.orange.es/orangetv.orange.es.config.js index f7e675a..389f3f7 100644 --- a/fixes/orangetv.orange.es/orangetv.orange.es.config.js +++ b/fixes/orangetv.orange.es/orangetv.orange.es.config.js @@ -9,10 +9,12 @@ dayjs.extend(utc) doFetch.setDebugger(debug) -const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO' -const API_CHANNEL_ENDPOINT = - 'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json' +const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/SmartTV_Android/1_PRO' const API_IMAGE_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images' +const API_CHANNEL_ENDPOINT = + 'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_external_id=1_PRO&model_external_id=PC&filter_unsupported_channels=true&max_pr_level=8&client=json' + +const caches = {} module.exports = { site: 'orangetv.orange.es', @@ -25,20 +27,31 @@ module.exports = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36' } }, - url: function({ date, segment = 1 }) { - return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_${segment}.json` + url({ date }) { + return segmentUrl(date) }, async parser({ content, channel, date }) { const programs = [] const items = parseItems(content, channel) - if (items.length) { - const queues = [ - module.exports.url({ date, segment: 2 }), - module.exports.url({ date, segment: 3 }) - ] - await doFetch(queues, (url, res) => { + const queues = [] + // fetch other segments or use cache if exist + for (let i = 2; i <= 3; i++) { + const url = segmentUrl(date, i) + if (caches[url] !== undefined) { + items.push(...parseItems(caches[url], channel)) + } else { + queues.push({ url, params: module.exports.request }) + } + } + if (queues.length) { + await doFetch(queues, (queue, res) => { + if (caches[queue.url] === undefined) { + caches[queue.url] = res + } items.push(...parseItems(res, channel)) }) + } + if (items.length) { programs.push( ...items.map(item => { return { @@ -75,6 +88,10 @@ module.exports = { } } +function segmentUrl(date, segment = 1) { + return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_${segment}.json` +} + function parseIcon(item) { if (item.attachments.length) { const cover = item.attachments.find(i => i.name.match(/cover/i)) @@ -94,7 +111,7 @@ function parseItems(content, channel) { typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : [] if (Array.isArray(json)) { json - .filter(i => i.channelExternalId === channel.site_id) + .filter(i => String(i.channelExternalId) === String(channel.site_id)) .forEach(i => { result.push(...i.programs) }) diff --git a/fixes/telenet.tv/telenet.tv.config.js b/fixes/telenet.tv/telenet.tv.config.js index cf9117a..acda9fe 100644 --- a/fixes/telenet.tv/telenet.tv.config.js +++ b/fixes/telenet.tv/telenet.tv.config.js @@ -1,135 +1,215 @@ -//https://github.com/iptv-org/epg/blob/e4f92bb2a2768dcba3dbbd52b19d78d96bebc31e/sites/telenet.tv/telenet.tv.config.js +//https://github.com/iptv-org/epg/blob/master/sites/web.magentatv.de/web.magentatv.de.config.js const axios = require('axios') const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') -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' +let X_CSRFTOKEN +let Cookie +const cookiesToExtract = ['JSESSIONID', 'CSESSIONID', 'CSRFSESSION'] + +dayjs.extend(utc) +dayjs.extend(customParseFormat) module.exports = { - site: 'telenet.tv', + site: 'web.magentatv.de', days: 2, + url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList', request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDD')}000000` - }, - 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.format('YYYYMMDD')}060000`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDD')}120000`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDD')}180000`, - { - 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) + method: 'POST', + async headers() { + return await setHeaders() + }, + data({ channel, date }) { + return { + count: -1, + isFillProgram: 1, + offset: 0, + properties: [ + { + include: + 'endtime,genres,id,name,starttime,channelid,pictures,introduce,subName,seasonNum,subNum,cast,country,producedate,externalIds', + name: 'playbill' } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item, channel) - programs.push({ - title: item.title, - subTitle: detail.episodeName, - icon: parseIcon(item), - description: detail.longDescription, - category: detail.genres, - actors: detail.actors, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) + ], + type: 2, + begintime: date.format('YYYYMMDD000000'), + channelid: channel.site_id, + endtime: date.add(1, 'd').format('YYYYMMDD000000') + } } - + }, + parser({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.introduce, + image: parseImage(item), + icon: parseImage(item), + category: parseCategory(item), + start: parseStart(item), + stop: parseStop(item), + sub_title: item.subName, + season: item.seasonNum, + episode: item.subNum, + directors: parseDirectors(item), + producers: parseProducers(item), + adapters: parseAdapters(item), + country: item.country?.toUpperCase(), + date: item.producedate, + urls: parseUrls(item) + }) + }) return programs }, async channels() { + const url = 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel' + const body = { + channelNamespace: 2, + filterlist: [ + { + key: 'IsHide', + value: '-1' + } + ], + metaDataVer: 'Channel/1.1', + properties: [ + { + include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name', + name: 'logicalChannel' + } + ], + returnSatChannel: 0 + } + const params = { + headers: await setHeaders() + } + const data = await axios - .get(`${API_PROD_ENDPOINT}/channels?cityId=28001&language=en&productClass=Orion-DASH`) + .post(url, body, params) .then(r => r.data) .catch(console.log) - return data.map(item => { + return data.channellist.map(item => { return { - lang: 'nl', - site_id: item.id, + lang: 'de', + site_id: item.contentId, 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) +function parseCategory(item) { + return item.genres + ? item.genres + .replace('und', ',') + .split(',') + .map(i => i.trim()) + : [] +} - return data || {} +function parseDirectors(item) { + if (!item.cast || !item.cast.director) return [] + return item.cast.director + .replace('und', ',') + .split(',') + .map(i => i.trim()) +} + +function parseProducers(item) { + if (!item.cast || !item.cast.producer) return [] + return item.cast.producer + .replace('und', ',') + .split(',') + .map(i => i.trim()) +} + +function parseAdapters(item) { + if (!item.cast || !item.cast.adaptor) return [] + return item.cast.adaptor + .replace('und', ',') + .split(',') + .map(i => i.trim()) +} + +function parseUrls(item) { + // currently only a imdb id is returned by the api, thus we can construct the url here + if (!item.externalIds) return [] + return JSON.parse(item.externalIds) + .filter(externalId => externalId.type === 'imdb' && externalId.id) + .map(externalId => ({ system: 'imdb', value: `https://www.imdb.com/title/${externalId.id}` })) +} + +function parseImage(item) { + if (!Array.isArray(item.pictures) || !item.pictures.length) return null + + return item.pictures[0].href } function parseStart(item) { - return dayjs.unix(item.startTime) + return dayjs.utc(item.starttime, 'YYYY-MM-DD HH:mm:ss') } function parseStop(item) { - return dayjs.unix(item.endTime) + return dayjs.utc(item.endtime, 'YYYY-MM-DD HH:mm:ss') } -function parseItems(content, channel) { - if (!content) return [] +function parseItems(content) { 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 [] + if (!data || !Array.isArray(data.playbilllist)) return [] - return Array.isArray(channelData.events) ? channelData.events : [] + return data.playbilllist } -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber +async function fetchCookieAndToken() { + // Only fetch the cookies and csrfToken if they are not already set + if (X_CSRFTOKEN && Cookie) { + return + } + + try { + const response = await axios.request({ + url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate', + params: { + SID: 'firstup', + T: 'Windows_chrome_118' + }, + method: 'POST', + data: '{"terminalid":"00:00:00:00:00:00","mac":"00:00:00:00:00:00","terminaltype":"WEBTV","utcEnable":1,"timezone":"Etc/GMT0","userType":3,"terminalvendor":"Unknown"}' + }) + + // Extract the cookies specified in cookiesToExtract + const setCookieHeader = response.headers['set-cookie'] || [] + const extractedCookies = [] + cookiesToExtract.forEach(cookieName => { + const regex = new RegExp(`${cookieName}=(.+?)(;|$)`) + const match = setCookieHeader.find(header => regex.test(header)) + + if (match) { + const cookieString = regex.exec(match)[0] + extractedCookies.push(cookieString) + } + }) + + // check if we recieved a csrfToken only then store the values + if (!response.data.csrfToken) { + console.log('csrfToken not found in the response.') + return + } + + X_CSRFTOKEN = response.data.csrfToken + Cookie = extractedCookies.join(' ') + } catch (error) { + console.error(error) + } } -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} +async function setHeaders() { + await fetchCookieAndToken() -function parseIcon(item) { - return `${API_IMAGE_ENDPOINT}/intent/${item.id}/posterTile` -} \ No newline at end of file + return { X_CSRFTOKEN, Cookie } +}