Files
epg/sites/www.magenta.tv/www.magenta.tv.config.js
T

355 lines
9.5 KiB
JavaScript

const axios = require('axios')
const crypto = require('crypto')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
const MANIFEST_URL = 'https://prod.dcm.telekom-dienste.de/v1/settings/web-mtv/manifest'
const DEVICE_MODEL = 'WEB2_FTV'
const PORTAL = 'release'
const SUBSCRIBER_TYPE = 'FTV_FREEMIUM_DT'
const CHANNEL_PAGE_SIZE = 100
const CHANNEL_PAGE_LIMIT = 1000
const FALLBACK_MPX = Object.freeze({
accountPid: 'mdeprod',
locationIdUri: 'http://data.entertainment.tv.theplatform.eu/entertainment/data/Location/245991976396',
feeds: {
allChannelSchedulesFeed:
'https://feed.entertainment.tv.theplatform.eu/f/{MpxAccountPid}/{MpxAccountPid}-all-channel-schedules',
allChannelStationsFeed:
'https://feed.entertainment.tv.theplatform.eu/f/{MpxAccountPid}/{MpxAccountPid}-channel-stations-main'
}
})
let session
let manifestPromise
module.exports = {
site: 'www.magenta.tv',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
async url({ channel, date }) {
const mpx = await getMpxConfig()
const currentSession = getSession()
const window = getUtcWindow(date)
return buildScheduleUrl({
mpx,
session: currentSession,
siteIds: [channel.site_id],
window
})
},
async parser({ content, channel }) {
return parseScheduleResponse(content, channel)
},
async channels() {
const mpx = await getMpxConfig()
const currentSession = getSession()
const channels = []
for (let start = 1; start <= CHANNEL_PAGE_LIMIT; start += CHANNEL_PAGE_SIZE) {
const entries = await getChannelEntries({
mpx,
session: currentSession,
start,
end: start + CHANNEL_PAGE_SIZE - 1
})
if (!entries.length) break
entries.forEach(entry => {
const channel = parseChannel(entry)
if (channel) channels.push(channel)
})
if (entries.length < CHANNEL_PAGE_SIZE) break
}
return channels
}
}
function createSession() {
return {
deviceId: crypto.randomUUID(),
sessionId: crypto.randomUUID()
}
}
function getSession() {
if (!session) {
session = createSession()
}
return session
}
function buildCid(currentSession) {
return `${currentSession.sessionId}::${crypto.randomUUID()}`
}
function getManifestRequestConfig(currentSession) {
return {
headers: {
'X-DT-Call-ID': crypto.randomUUID(),
'X-DT-Session-ID': currentSession.sessionId
},
params: {
deviceId: currentSession.deviceId,
deviceModel: DEVICE_MODEL,
portal: PORTAL,
subscriberType: SUBSCRIBER_TYPE,
$redirect: false,
sid: currentSession.sessionId
}
}
}
async function getManifest() {
if (!manifestPromise) {
manifestPromise = axios
.get(MANIFEST_URL, getManifestRequestConfig(getSession()))
.then(r => r.data)
.catch(() => null)
}
return manifestPromise
}
async function getMpxConfig() {
const manifest = await getManifest()
const manifestMpx = manifest && manifest.mpx ? manifest.mpx : {}
return {
...FALLBACK_MPX,
...manifestMpx,
feeds: {
...FALLBACK_MPX.feeds,
...(manifestMpx.feeds || {})
}
}
}
async function getChannelEntries({ mpx, session, start, end }) {
const url = buildChannelFeedUrl({ mpx, session, start, end })
const data = await axios
.get(url)
.then(r => r.data)
.catch(() => null)
return Array.isArray(data && data.entries) ? data.entries : []
}
function buildChannelFeedUrl({ mpx, session, start, end }) {
const url = new URL(resolveFeedTemplate(mpx.feeds.allChannelStationsFeed, mpx.accountPid))
url.searchParams.set('lang', 'short-de')
url.searchParams.set('sort', 'dt$displayChannelNumber')
url.searchParams.set('range', `${start}-${end}`)
url.searchParams.set('cid', buildCid(session))
return url.toString()
}
function buildScheduleUrl({ mpx, session, siteIds, window }) {
const url = new URL(resolveFeedTemplate(mpx.feeds.allChannelSchedulesFeed, mpx.accountPid))
url.searchParams.set('byId', siteIds.join('|'))
url.searchParams.set('byListingTime', window)
url.searchParams.set('byLocationId', mpx.locationIdUri)
url.searchParams.set('cid', buildCid(session))
return url.toString()
}
function resolveFeedTemplate(template, accountPid) {
return template.replaceAll('{MpxAccountPid}', accountPid)
}
function getUtcWindow(date) {
const start = date.utc().startOf('day')
const end = start.add(1, 'day')
return `${start.toISOString()}~${end.toISOString()}`
}
function parseScheduleResponse(content, channel) {
const data = parseJson(content)
const entries = Array.isArray(data && data.entries) ? data.entries : []
const targetSiteId = channel && channel.site_id ? String(channel.site_id) : null
const programs = []
entries.forEach(entry => {
if (targetSiteId && extractNumericId(entry.id) !== targetSiteId) return
if (!Array.isArray(entry.listings)) return
entry.listings.forEach(listing => {
const program = parseProgramme(entry, listing)
if (program) programs.push(program)
})
})
return programs
}
function parseChannel(entry) {
if (!entry || entry['dt$isRadio']) return null
const station = getFirstStation(entry)
const siteId = extractNumericId(entry.id)
const name = station && station.title ? station.title : entry.title
if (!station || !siteId || !name) return null
return {
lang: 'de',
site_id: siteId,
name
}
}
function parseProgramme(entry, listing) {
if (
!listing ||
!listing.program ||
listing.startTime === null ||
listing.startTime === undefined ||
listing.endTime === null ||
listing.endTime === undefined
) {
return null
}
const program = listing.program
const programInfo = parseProgramInfo(listing['dt$programInfo'])
const title = program.title
if (!title) return null
const parsed = {
title,
description: program.description || null,
category: parseCategories(program.tags),
sub_title: parseSubTitle(program, listing),
rating: parseRating(program.ratings),
season: normalizeNumber(program.tvSeasonNumber ?? programInfo.tvSeasonNumber),
episode: normalizeNumber(
program.tvSeasonEpisodeNumber ??
programInfo.tvSeasonEpisodeNumber ??
programInfo.seriesEpisodeNumber
),
image: parseProgramImage(program),
icon: parseProgramImage(program),
start: dayjs(Number(listing.startTime)),
stop: dayjs(Number(listing.endTime)),
country: parseCountry(program['dt$countries']),
date: program.year ? String(program.year) : null
}
if (parsed.image === null) delete parsed.image
if (parsed.icon === null) delete parsed.icon
if (parsed.category === null) delete parsed.category
if (parsed.sub_title === null) delete parsed.sub_title
if (parsed.rating === null) delete parsed.rating
if (parsed.season === null) delete parsed.season
if (parsed.episode === null) delete parsed.episode
if (parsed.country === null) delete parsed.country
if (parsed.date === null) delete parsed.date
return parsed
}
function parseSubTitle(program, listing) {
if (program.secondaryTitle && program.secondaryTitle !== program.title) {
return program.secondaryTitle
}
if (listing['dt$seriesTitle'] && listing['dt$seriesTitle'] !== program.title) {
return listing['dt$seriesTitle']
}
return null
}
function parseCategories(tags) {
if (!Array.isArray(tags)) return null
const categories = tags
.filter(tag => ['category', 'genre-primary', 'genre-secondary'].includes(tag.scheme) && tag.title)
.map(tag => tag.title)
return categories.length ? [...new Set(categories)] : null
}
function parseRating(ratings) {
if (!Array.isArray(ratings)) return null
const rating = ratings.find(item => item && item.rating && item.rating !== 'UNKNOWN')
if (!rating) return null
return {
system: rating.scheme || 'magenta',
value: rating.rating
}
}
function parseProgramInfo(value) {
if (!value || typeof value !== 'string') return {}
try {
return JSON.parse(value.replaceAll("'", '"'))
} catch {
return {}
}
}
function parseProgramImage(program) {
if (!program || !program.thumbnails) return null
const thumbnails = Object.values(program.thumbnails)
.filter(thumbnail => thumbnail && thumbnail.url)
.sort((a, b) => (b.width || 0) * (b.height || 0) - (a.width || 0) * (a.height || 0))
return thumbnails[0] ? thumbnails[0].url : null
}
function parseCountry(value) {
if (!value || typeof value !== 'string') return null
return value.toUpperCase()
}
function getFirstStation(entry) {
if (!entry || !entry.stations || typeof entry.stations !== 'object') return null
return Object.values(entry.stations)[0] || null
}
function extractNumericId(uri) {
if (!uri || typeof uri !== 'string') return null
const match = uri.match(/(\d+)(?!.*\d)/)
return match ? match[1] : null
}
function normalizeNumber(value) {
return value === null || value === undefined || value === '' ? null : value
}
function parseJson(content) {
if (!content) return {}
if (typeof content !== 'string') return content
try {
return JSON.parse(content)
} catch {
return {}
}
}