const doFetch = require('@ntlab/sfetch') const axios = require('axios') const dayjs = require('dayjs') const crypto = require('crypto') const sortBy = require('lodash.sortby') // API Configuration Constants const NATCO_CODE = 'hr' const APP_LANGUAGE = 'hr' const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5' const APP_VERSION = '02.0.1470' const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE' const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr' // Role Types const ROLE_TYPES = { ACTOR: 'GLUMI', // Croatian for "ACTS" DIRECTOR: 'REĊ½IJA', // Croatian for "DIRECTOR" PRODUCER: 'PRODUKCIJA', // Croatian for "PRODUCER" WRITER: 'AUTOR', SCENARIO: 'SCENARIJ' } // 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 = {} module.exports = { site: SITE_URL, url({ date }) { return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format( 'YYYY-MM-DD' )}&hour_offset=0&hour_range=3&channelMap_id=&filler=true&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}` }, request: { headers: getHeaders(), cache: { ttl: 24 * 60 * 60 * 1000 // 1 day } }, async parser({ content, channel, date }) { const data = parseData(content) if (!data) return [] let items = parseItems(data, channel) if (!items.length) return [] 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, headers: getHeaders() } if (cached[url]) { items = items.concat(parseItems(cached[url], channel)) return null } return { url, params } }) .filter(Boolean) 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()) // Fetch program details for each item const programs = [] const queues = items .filter(item => item.program_id) .map(item => ({ url: `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}`, params: { headers: getHeaders() }, item })) await doFetch(queues, (queue, res) => { queue.item.detail = res }) items.forEach(item => { const detail = item.detail || {} programs.push({ title: item.description, sub_title: item.episode_name, description: parseDescription(detail), categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [], date: parseDate(item), image: detail.poster_image_url, actors: parseRoles(detail, ROLE_TYPES.ACTOR), directors: parseRoles(detail, ROLE_TYPES.DIRECTOR), producers: parseRoles(detail, ROLE_TYPES.PRODUCER), 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( `${API_ENDPOINT}/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=${NATCO_KEY}&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`, { ...module.exports.request, headers: getHeaders() } ) .then(r => r.data) .catch(console.error) return data.channels.map(channel => ({ lang: NATCO_CODE, name: channel.title, site_id: channel.station_id })) } } function parseData(content) { try { const data = JSON.parse(content) return data || null } catch { return null } } 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 parseRoles(item, role_name) { if (!item.roles) return null return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) } function getHeaders() { const res = { 'app_key': APP_KEY, 'app_version': APP_VERSION, 'device-id': DEVICE_ID, 'tenant': 'tv', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36', 'origin': `https://${SITE_URL}`, 'x-call-type': 'GUEST_USER', 'x-call-time': new Date().getTime(), 'x-request-session-id': SESSION_ID, 'x-request-tracking-id': crypto.randomUUID(), 'x-tv-step': 'EPG_SCHEDULES', 'x-tv-flow': 'EPG', 'x-txn-id': null, 'x-user-agent': `web|web|Chrome-149|${APP_VERSION}|1` } const hash = crypto.createHash('sha256') hash.update([ res['x-request-tracking-id'], res['x-request-session-id'], res['device-id'], res['x-call-time'] ].join('')) res['x-txn-id'] = hash.digest('hex').substr(0, 32) return res }