diff --git a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js index 0d44fcd1..647f58f0 100644 --- a/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js +++ b/sites/mojmaxtv.hrvatskitelekom.hr/mojmaxtv.hrvatskitelekom.hr.config.js @@ -2,31 +2,48 @@ const doFetch = require('@ntlab/sfetch') const axios = require('axios') const dayjs = require('dayjs') const _ = require('lodash') +const crypto = require('crypto') + +// API Configuration Constants +const NATCO_CODE = 'hr' +const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5' +const APP_VERSION = '02.0.1080' +const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE' +const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr' + +// Dynamic API Endpoint based on NATCO_CODE +const API_ENDPOINT = `https://tv-${NATCO_CODE}-prod.yo-digital.com/${NATCO_CODE}-bifrost` + +// Session/Device IDs +const DEVICE_ID = crypto.randomUUID() +const SESSION_ID = crypto.randomUUID() const cached = {} +const getHeaders = () => ({ + 'app_key': APP_KEY, + 'app_version': APP_VERSION, + 'device-id': DEVICE_ID, + 'tenant': 'tv', + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', + 'origin': `https://${SITE_URL}`, + 'x-request-session-id': SESSION_ID, + 'x-request-tracking-id': crypto.randomUUID(), + 'x-tv-step': 'EPG_SCHEDULES', + 'x-tv-flow': 'EPG', + 'x-call-type': 'GUEST_USER', + 'x-user-agent': `web|web|Chrome-133|${APP_VERSION}|1` +}) + module.exports = { - site: 'mojmaxtv.hrvatskitelekom.hr', + site: SITE_URL, url({ date }) { - return `https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel/schedules?date=${date.format( + return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format( 'YYYY-MM-DD' - )}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=hr&natco_code=hr` + )}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=${NATCO_CODE}&natco_code=${NATCO_CODE}` }, request: { - headers: { - 'app_key': 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5', - 'app_version': '02.0.1080', - 'device-id': 'a78f079d-5527-46d8-af3f-9f0b6b6fb758', - 'tenant': 'tv', - 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', - 'origin': 'https://mojmaxtv.hrvatskitelekom.hr', - 'x-request-session-id': 'fc96c9de-7a3b-4b51-8b9d-5d9f9a3c3268', - 'x-request-tracking-id': '05a8f0bc-f977-4754-b8ad-1d4d1bd742fb', - 'x-tv-step': 'EPG_SCHEDULES', - 'x-tv-flow': 'EPG', - 'x-call-type': 'GUEST_USER', - 'x-user-agent': 'web|web|Chrome-133|02.0.1080|1' - }, + headers: getHeaders(), cache: { ttl: 24 * 60 * 60 * 1000 // 1 day } @@ -41,11 +58,10 @@ module.exports = { const queue = [3, 6, 9, 12, 15, 18, 21] .map(offset => { const url = module.exports.url({ date }).replace('hour_offset=0', `hour_offset=${offset}`) - const params = module.exports.request + const params = { ...module.exports.request, headers: getHeaders() } if (cached[url]) { items = items.concat(parseItems(cached[url], channel)) - return null } @@ -56,44 +72,66 @@ module.exports = { await doFetch(queue, (_req, _data) => { if (_data) { cached[_req.url] = _data - items = items.concat(parseItems(_data, channel)) } }) items = _.sortBy(items, i => dayjs(i.start_time).valueOf()) - return items.map(item => ({ - title: item.description, - categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [], - season: item.season_number, - episode: item.episode_number ? parseInt(item.episode_number) : null, - date: item['release_year'] ? item['release_year'].toString() : null, - start: item.start_time, - stop: item.end_time - })) + // Fetch program details for each item + const programs = [] + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.description, + description: parseDescription(detail), + categories: parseCategories(detail), + date: parseDate(item), + image: detail.poster_image_url, + actors: parseRoles(detail, 'GLUMI'), + directors: parseRoles(detail, 'REĊ½IJA'), + producers: parseRoles(detail, 'Producent'), + season: parseSeason(item), + episode: parseEpisode(item), + rating: parseRating(item), + start: item.start_time, + stop: item.end_time + }) + } + + return programs }, async channels() { const data = await axios .get( - 'https://tv-hr-prod.yo-digital.com/hr-bifrost/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE&app_language=hr&natco_code=hr', - module.exports.request + `${API_ENDPOINT}/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=${NATCO_KEY}&app_language=${NATCO_CODE}&natco_code=${NATCO_CODE}`, + { ...module.exports.request, headers: getHeaders() } ) .then(r => r.data) .catch(console.error) return data.channels.map(channel => ({ - lang: 'hr', + lang: NATCO_CODE, name: channel.title, site_id: channel.station_id })) } } +async function loadProgramDetails(item) { + if (!item.program_id) return {} + const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}` + const data = await axios + .get(url, { headers: getHeaders() }) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + function parseData(content) { try { const data = JSON.parse(content) - return data || null } catch { return null @@ -102,6 +140,51 @@ function parseData(content) { function parseItems(data, channel) { if (!data.channels || !Array.isArray(data.channels[channel.site_id])) return [] - return data.channels[channel.site_id] } + +function parseDate(item) { + return item && item.release_year ? item.release_year.toString() : null +} + +function parseRating(item) { + return item.ratings + ? { + system: 'MPA', + value: item.ratings + } + : null +} + +function parseSeason(item) { + if (item.season_display_number === 'Epizode') return null // 'Epizode' is 'Episodes' in Croatian + return item.season_number +} + +function parseEpisode(item) { + if (item.episode_number) return parseInt(item.episode_number) + if (item.season_display_number === 'Epizode') return item.season_number + return null +} + +function parseDescription(item) { + if (!item.details) return null + return item.details.description +} + +function parseCategories(item) { + if (!item.details?.metadata) return [] + + const genreMetadata = item.details.metadata.find(meta => + meta.type === "GENRES" + ) + + if (!genreMetadata?.value) return [] + + return genreMetadata.value.split(', ').filter(Boolean) +} + +function parseRoles(item, role_name) { + if (!item.roles) return null + return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) +}