Replace LF endings with CRLF

This commit is contained in:
freearhey
2025-07-31 22:29:01 +03:00
parent 17e3b4ddda
commit 29aa427923
379 changed files with 29332 additions and 29332 deletions

View File

@@ -1,45 +1,45 @@
import { Logger, Storage } from '@freearhey/core' import { Logger, Storage } from '@freearhey/core'
import { SITES_DIR } from '../../constants' import { SITES_DIR } from '../../constants'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import { program } from 'commander' import { program } from 'commander'
import fs from 'fs-extra' import fs from 'fs-extra'
program.argument('<site>', 'Domain name of the site').parse(process.argv) program.argument('<site>', 'Domain name of the site').parse(process.argv)
const domain = program.args[0] const domain = program.args[0]
async function main() { async function main() {
const storage = new Storage(SITES_DIR) const storage = new Storage(SITES_DIR)
const logger = new Logger() const logger = new Logger()
logger.info(`Initializing "${domain}"...\r\n`) logger.info(`Initializing "${domain}"...\r\n`)
const dir = domain const dir = domain
if (await storage.exists(dir)) { if (await storage.exists(dir)) {
throw new Error(`Folder "${dir}" already exists`) throw new Error(`Folder "${dir}" already exists`)
} }
await storage.createDir(dir) await storage.createDir(dir)
logger.info(`Creating "${dir}/${domain}.test.js"...`) logger.info(`Creating "${dir}/${domain}.test.js"...`)
const testTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_test.js'), { const testTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_test.js'), {
encoding: 'utf8' encoding: 'utf8'
}) })
await storage.save(`${dir}/${domain}.test.js`, testTemplate.replace(/<DOMAIN>/g, domain)) await storage.save(`${dir}/${domain}.test.js`, testTemplate.replace(/<DOMAIN>/g, domain))
logger.info(`Creating "${dir}/${domain}.config.js"...`) logger.info(`Creating "${dir}/${domain}.config.js"...`)
const configTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_config.js'), { const configTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_config.js'), {
encoding: 'utf8' encoding: 'utf8'
}) })
await storage.save(`${dir}/${domain}.config.js`, configTemplate.replace(/<DOMAIN>/g, domain)) await storage.save(`${dir}/${domain}.config.js`, configTemplate.replace(/<DOMAIN>/g, domain))
logger.info(`Creating "${dir}/readme.md"...`) logger.info(`Creating "${dir}/readme.md"...`)
const readmeTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_readme.md'), { const readmeTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_readme.md'), {
encoding: 'utf8' encoding: 'utf8'
}) })
await storage.save(`${dir}/readme.md`, readmeTemplate.replace(/<DOMAIN>/g, domain)) await storage.save(`${dir}/readme.md`, readmeTemplate.replace(/<DOMAIN>/g, domain))
logger.info('\r\nDone') logger.info('\r\nDone')
} }
main() main()

View File

@@ -1,9 +1,9 @@
export const ROOT_DIR = process.env.ROOT_DIR || '.' export const ROOT_DIR = process.env.ROOT_DIR || '.'
export const SITES_DIR = process.env.SITES_DIR || './sites' export const SITES_DIR = process.env.SITES_DIR || './sites'
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
export const DATA_DIR = process.env.DATA_DIR || './temp/data' export const DATA_DIR = process.env.DATA_DIR || './temp/data'
export const API_DIR = process.env.API_DIR || '.api' export const API_DIR = process.env.API_DIR || '.api'
export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites' export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites'
export const TESTING = process.env.NODE_ENV === 'test' ? true : false export const TESTING = process.env.NODE_ENV === 'test' ? true : false
export const OWNER = 'iptv-org' export const OWNER = 'iptv-org'
export const REPO = 'epg' export const REPO = 'epg'

View File

@@ -1,16 +1,16 @@
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios' import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
export class ApiClient { export class ApiClient {
instance: AxiosInstance instance: AxiosInstance
constructor() { constructor() {
this.instance = axios.create({ this.instance = axios.create({
baseURL: 'https://iptv-org.github.io/api', baseURL: 'https://iptv-org.github.io/api',
responseType: 'stream' responseType: 'stream'
}) })
} }
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> { get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
return this.instance.get(url, options) return this.instance.get(url, options)
} }
} }

View File

@@ -1,14 +1,14 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
dayjs.extend(utc) dayjs.extend(utc)
const date = {} const date = {}
date.getUTC = function (d = null) { date.getUTC = function (d = null) {
if (typeof d === 'string') return dayjs.utc(d).startOf('d') if (typeof d === 'string') return dayjs.utc(d).startOf('d')
return dayjs.utc().startOf('d') return dayjs.utc().startOf('d')
} }
export default date export default date

View File

@@ -1,34 +1,34 @@
import { Dictionary } from '@freearhey/core' import { Dictionary } from '@freearhey/core'
import { Issue } from '../models' import { Issue } from '../models'
const FIELDS = new Dictionary({ const FIELDS = new Dictionary({
Site: 'site' Site: 'site'
}) })
export class IssueParser { export class IssueParser {
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue { parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
const fields = issue.body.split('###') const fields = issue.body.split('###')
const data = new Dictionary() const data = new Dictionary()
fields.forEach((field: string) => { fields.forEach((field: string) => {
const parsed = field.split(/\r?\n/).filter(Boolean) const parsed = field.split(/\r?\n/).filter(Boolean)
let _label = parsed.shift() let _label = parsed.shift()
_label = _label ? _label.trim() : '' _label = _label ? _label.trim() : ''
let _value = parsed.join('\r\n') let _value = parsed.join('\r\n')
_value = _value ? _value.trim() : '' _value = _value ? _value.trim() : ''
if (!_label || !_value) return data if (!_label || !_value) return data
const id: string = FIELDS.get(_label) const id: string = FIELDS.get(_label)
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
if (!id) return if (!id) return
data.set(id, value) data.set(id, value)
}) })
const labels = issue.labels.map(label => label.name) const labels = issue.labels.map(label => label.name)
return new Issue({ number: issue.number, labels, data }) return new Issue({ number: issue.number, labels, data })
} }
} }

View File

@@ -1,16 +1,16 @@
module.exports = { module.exports = {
site: '<DOMAIN>', site: '<DOMAIN>',
url({ channel, date }) { url({ channel, date }) {
return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}` return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}`
}, },
parser({ content }) { parser({ content }) {
try { try {
return JSON.parse(content) return JSON.parse(content)
} catch { } catch {
return [] return []
} }
}, },
channels() { channels() {
return [] return []
} }
} }

View File

@@ -1,38 +1,38 @@
const { parser, url } = require('./<DOMAIN>.config.js') const { parser, url } = require('./<DOMAIN>.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'bbc1' } const channel = { site_id: 'bbc1' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12') expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content =
'[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]' '[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]'
const results = parser({ content }) const results = parser({ content })
expect(results.length).toBe(2) expect(results.length).toBe(2)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'Program 1', title: 'Program 1',
start: '2025-01-12T00:00:00.000Z', start: '2025-01-12T00:00:00.000Z',
stop: '2025-01-12T00:30:00.000Z' stop: '2025-01-12T00:30:00.000Z'
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
title: 'Program 2', title: 'Program 2',
start: '2025-01-12T00:30:00.000Z', start: '2025-01-12T00:30:00.000Z',
stop: '2025-01-12T01:00:00.000Z' stop: '2025-01-12T01:00:00.000Z'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: '' }) const results = parser({ content: '' })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1 +1 @@
declare module 'langs' declare module 'langs'

View File

@@ -1,69 +1,69 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: '9tv.co.il', site: '9tv.co.il',
days: 2, days: 2,
url: function ({ date }) { url: function ({ date }) {
return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format(
'DD/MM/YYYY 00:00:00' 'DD/MM/YYYY 00:00:00'
)}` )}`
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
const start = parseStart($item, date) const start = parseStart($item, date)
if (prev) prev.stop = start if (prev) prev.stop = start
const stop = start.add(1, 'h') const stop = start.add(1, 'h')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
image: parseImage($item), image: parseImage($item),
description: parseDescription($item), description: parseDescription($item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseStart($item, date) { function parseStart($item, date) {
let time = $item('a > div.guide_list_time').text().trim() let time = $item('a > div.guide_list_time').text().trim()
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem') return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem')
} }
function parseImage($item) { function parseImage($item) {
const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css(
'background-image' 'background-image'
) )
if (!backgroundImage) return null if (!backgroundImage) return null
const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null]
return relativePath ? `https://www.9tv.co.il${relativePath}` : null return relativePath ? `https://www.9tv.co.il${relativePath}` : null
} }
function parseDescription($item) { function parseDescription($item) {
return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim()
} }
function parseTitle($item) { function parseTitle($item) {
return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim()
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('li').toArray() return $('li').toArray()
} }

View File

@@ -1,122 +1,122 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'abc.net.au', site: 'abc.net.au',
days: 3, days: 3,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ date, channel }) { url({ date, channel }) {
const [region] = channel.site_id.split('#') const [region] = channel.site_id.split('#')
return `https://cdn.iview.abc.net.au/epg/processed/${region}_${date.format('YYYY-MM-DD')}.json` return `https://cdn.iview.abc.net.au/epg/processed/${region}_${date.format('YYYY-MM-DD')}.json`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title, title: item.title,
sub_title: item.episode_title, sub_title: item.episode_title,
category: item.genres, category: item.genres,
description: item.description, description: item.description,
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
rating: parseRating(item), rating: parseRating(item),
image: parseImage(item), image: parseImage(item),
start: parseTime(item.start_time), start: parseTime(item.start_time),
stop: parseTime(item.end_time) stop: parseTime(item.end_time)
}) })
}) })
return programs return programs
}, },
async channels({ region = 'syd' }) { async channels({ region = 'syd' }) {
const now = dayjs() const now = dayjs()
const regions = { const regions = {
syd: 'Sydney', syd: 'Sydney',
mel: 'Melbourne', mel: 'Melbourne',
bri: 'Brisbane', bri: 'Brisbane',
gc: 'GoldCoast', gc: 'GoldCoast',
per: 'Perth', per: 'Perth',
adl: 'Adelaide', adl: 'Adelaide',
hbr: 'Hobart', hbr: 'Hobart',
drw: 'Darwin', drw: 'Darwin',
cbr: 'Canberra', cbr: 'Canberra',
nsw: 'New South Wales', nsw: 'New South Wales',
vic: 'Victoria', vic: 'Victoria',
tsv: 'Townsville', tsv: 'Townsville',
qld: 'Queensland', qld: 'Queensland',
wa: 'Western Australia', wa: 'Western Australia',
sa: 'South Australia', sa: 'South Australia',
tas: 'Tasmania', tas: 'Tasmania',
nt: 'Northern Territory' nt: 'Northern Territory'
} }
let channels = [] let channels = []
const regionName = regions[region] const regionName = regions[region]
const data = await axios const data = await axios
.get( .get(
`https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json` `https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json`
) )
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
for (let item of data.schedule) { for (let item of data.schedule) {
channels.push({ channels.push({
lang: 'en', lang: 'en',
site_id: `${regionName}#${item.channel}`, site_id: `${regionName}#${item.channel}`,
name: item.channel name: item.channel
}) })
} }
return channels return channels
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data) return [] if (!data) return []
if (!Array.isArray(data.schedule)) return [] if (!Array.isArray(data.schedule)) return []
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const channelData = data.schedule.find(i => i.channel == channelId) const channelData = data.schedule.find(i => i.channel == channelId)
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
} catch { } catch {
return [] return []
} }
} }
function parseSeason(item) { function parseSeason(item) {
return item.series_num || null return item.series_num || null
} }
function parseEpisode(item) { function parseEpisode(item) {
return item.episode_num || null return item.episode_num || null
} }
function parseTime(time) { function parseTime(time) {
return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney') return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney')
} }
function parseImage(item) { function parseImage(item) {
return item.image_file return item.image_file
? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}` ? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}`
: null : null
} }
function parseRating(item) { function parseRating(item) {
return item.rating return item.rating
? { ? {
system: 'ACB', system: 'ACB',
value: item.rating value: item.rating
} }
: null : null
} }

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./abc.net.au.config.js') const { parser, url } = require('./abc.net.au.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'Sydney#ABC1' } const channel = { site_id: 'Sydney#ABC1' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json' 'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => { const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(30) expect(results.length).toBe(30)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: "Julia Zemiro's Home Delivery", title: "Julia Zemiro's Home Delivery",
sub_title: 'Maggie Beer', sub_title: 'Maggie Beer',
description: description:
"The kitchen Maggie Beer made famous in The Cook and the Chef may be in the heart of the Barossa Valley, but our most beloved foodie meets up with Julia where she grew up in Sydney's Lakemba.", "The kitchen Maggie Beer made famous in The Cook and the Chef may be in the heart of the Barossa Valley, but our most beloved foodie meets up with Julia where she grew up in Sydney's Lakemba.",
category: ['Entertainment', 'Factual'], category: ['Entertainment', 'Factual'],
rating: { rating: {
system: 'ACB', system: 'ACB',
value: 'G' value: 'G'
}, },
season: null, season: null,
episode: null, episode: null,
image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg', image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg',
start: '2025-02-03T12:40:00.000Z', start: '2025-02-03T12:40:00.000Z',
stop: '2025-02-03T13:09:00.000Z' stop: '2025-02-03T13:09:00.000Z'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')),
channel channel
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,65 +1,65 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'allente.dk', site: 'allente.dk',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ date }) { url({ date }) {
return `https://cs-vcb.allente.dk/epg/events?date=${date.format('YYYY-MM-DD')}` return `https://cs-vcb.allente.dk/epg/events?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
if (!item.details) return if (!item.details) return
const start = dayjs(item.time) const start = dayjs(item.time)
const stop = start.add(item.details.duration, 'm') const stop = start.add(item.details.duration, 'm')
programs.push({ programs.push({
title: item.title, title: item.title,
category: item.details.categories, category: item.details.categories,
description: item.details.description, description: item.details.description,
image: item.details.image, image: item.details.image,
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://cs-vcb.allente.dk/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) .get(`https://cs-vcb.allente.dk/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
return { return {
lang: 'da', lang: 'da',
site_id: item.id, site_id: item.id,
name: item.name name: item.name
} }
}) })
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.channels)) return [] if (!data || !Array.isArray(data.channels)) return []
const channelData = data.channels.find(i => i.id === channel.site_id) const channelData = data.channels.find(i => i.id === channel.site_id)
return channelData && Array.isArray(channelData.events) ? channelData.events : [] return channelData && Array.isArray(channelData.events) ? channelData.events : []
} }
function parseSeason(item) { function parseSeason(item) {
return item.details.season || null return item.details.season || null
} }
function parseEpisode(item) { function parseEpisode(item) {
return item.details.episode || null return item.details.episode || null
} }

View File

@@ -1,65 +1,65 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'allente.fi', site: 'allente.fi',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ date }) { url({ date }) {
return `https://cs-vcb.allente.fi/epg/events?date=${date.format('YYYY-MM-DD')}` return `https://cs-vcb.allente.fi/epg/events?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
if (!item.details) return if (!item.details) return
const start = dayjs(item.time) const start = dayjs(item.time)
const stop = start.add(item.details.duration, 'm') const stop = start.add(item.details.duration, 'm')
programs.push({ programs.push({
title: item.title, title: item.title,
category: item.details.categories, category: item.details.categories,
description: item.details.description, description: item.details.description,
image: item.details.image, image: item.details.image,
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://cs-vcb.allente.fi/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) .get(`https://cs-vcb.allente.fi/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
return { return {
lang: 'fi', lang: 'fi',
site_id: item.id, site_id: item.id,
name: item.name name: item.name
} }
}) })
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.channels)) return [] if (!data || !Array.isArray(data.channels)) return []
const channelData = data.channels.find(i => i.id === channel.site_id) const channelData = data.channels.find(i => i.id === channel.site_id)
return channelData && Array.isArray(channelData.events) ? channelData.events : [] return channelData && Array.isArray(channelData.events) ? channelData.events : []
} }
function parseSeason(item) { function parseSeason(item) {
return item.details.season || null return item.details.season || null
} }
function parseEpisode(item) { function parseEpisode(item) {
return item.details.episode || null return item.details.episode || null
} }

View File

@@ -1,65 +1,65 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'allente.no', site: 'allente.no',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ date }) { url({ date }) {
return `https://cs-vcb.allente.no/epg/events?date=${date.format('YYYY-MM-DD')}` return `https://cs-vcb.allente.no/epg/events?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
if (!item.details) return if (!item.details) return
const start = dayjs(item.time) const start = dayjs(item.time)
const stop = start.add(item.details.duration, 'm') const stop = start.add(item.details.duration, 'm')
programs.push({ programs.push({
title: item.title, title: item.title,
category: item.details.categories, category: item.details.categories,
description: item.details.description, description: item.details.description,
image: item.details.image, image: item.details.image,
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://cs-vcb.allente.no/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) .get(`https://cs-vcb.allente.no/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
return { return {
lang: 'no', lang: 'no',
site_id: item.id, site_id: item.id,
name: item.name name: item.name
} }
}) })
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.channels)) return [] if (!data || !Array.isArray(data.channels)) return []
const channelData = data.channels.find(i => i.id === channel.site_id) const channelData = data.channels.find(i => i.id === channel.site_id)
return channelData && Array.isArray(channelData.events) ? channelData.events : [] return channelData && Array.isArray(channelData.events) ? channelData.events : []
} }
function parseSeason(item) { function parseSeason(item) {
return item.details.season || null return item.details.season || null
} }
function parseEpisode(item) { function parseEpisode(item) {
return item.details.episode || null return item.details.episode || null
} }

View File

@@ -1,65 +1,65 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'allente.se', site: 'allente.se',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ date }) { url({ date }) {
return `https://cs-vcb.allente.se/epg/events?date=${date.format('YYYY-MM-DD')}` return `https://cs-vcb.allente.se/epg/events?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
if (!item.details) return if (!item.details) return
const start = dayjs(item.time) const start = dayjs(item.time)
const stop = start.add(item.details.duration, 'm') const stop = start.add(item.details.duration, 'm')
programs.push({ programs.push({
title: item.title, title: item.title,
category: item.details.categories, category: item.details.categories,
description: item.details.description, description: item.details.description,
image: item.details.image, image: item.details.image,
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://cs-vcb.allente.se/epg/events?date=${dayjs().format('YYYY-MM-DD')}`) .get(`https://cs-vcb.allente.se/epg/events?date=${dayjs().format('YYYY-MM-DD')}`)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
return { return {
lang: 'sv', lang: 'sv',
site_id: item.id, site_id: item.id,
name: item.name name: item.name
} }
}) })
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.channels)) return [] if (!data || !Array.isArray(data.channels)) return []
const channelData = data.channels.find(i => i.id === channel.site_id) const channelData = data.channels.find(i => i.id === channel.site_id)
return channelData && Array.isArray(channelData.events) ? channelData.events : [] return channelData && Array.isArray(channelData.events) ? channelData.events : []
} }
function parseSeason(item) { function parseSeason(item) {
return item.details.season || null return item.details.season || null
} }
function parseEpisode(item) { function parseEpisode(item) {
return item.details.episode || null return item.details.episode || null
} }

View File

@@ -1,59 +1,59 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'andorradifusio.ad', site: 'andorradifusio.ad',
days: 2, days: 2,
url({ channel }) { url({ channel }) {
return `https://www.andorradifusio.ad/programacio/${channel.site_id}` return `https://www.andorradifusio.ad/programacio/${channel.site_id}`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart(item, date) let start = parseStart(item, date)
if (prev) { if (prev) {
if (start < prev.start) { if (start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.plus({ hours: 1 }) const stop = start.plus({ hours: 1 })
programs.push({ programs.push({
title: item.title, title: item.title,
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseStart(item, date) { function parseStart(item, date) {
const dateString = `${date.format('MM/DD/YYYY')} ${item.time}` const dateString = `${date.format('MM/DD/YYYY')} ${item.time}`
return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC() return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC()
} }
function parseItems(content, date) { function parseItems(content, date) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase() const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase()
const column = $('.programacio-dia > h3 > .dia') const column = $('.programacio-dia > h3 > .dia')
.filter((i, el) => $(el).text() === day.slice(0, 6) + '.') .filter((i, el) => $(el).text() === day.slice(0, 6) + '.')
.first() .first()
.parent() .parent()
.parent() .parent()
const items = [] const items = []
const titles = column.find('p').toArray() const titles = column.find('p').toArray()
column.find('h4').each((i, time) => { column.find('h4').each((i, time) => {
items.push({ items.push({
time: $(time).text(), time: $(time).text(),
title: $(titles[i]).text() title: $(titles[i]).text()
}) })
}) })
return items return items
} }

View File

@@ -1,47 +1,47 @@
const { parser, url } = require('./andorradifusio.ad.config.js') const { parser, url } = require('./andorradifusio.ad.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'atv', site_id: 'atv',
xmltv_id: 'AndorraTV.ad' xmltv_id: 'AndorraTV.ad'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv') expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => { const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-06-07T05:00:00.000Z', start: '2023-06-07T05:00:00.000Z',
stop: '2023-06-07T06:00:00.000Z', stop: '2023-06-07T06:00:00.000Z',
title: 'Club Piolet' title: 'Club Piolet'
}) })
expect(results[20]).toMatchObject({ expect(results[20]).toMatchObject({
start: '2023-06-07T23:00:00.000Z', start: '2023-06-07T23:00:00.000Z',
stop: '2023-06-08T00:00:00.000Z', stop: '2023-06-08T00:00:00.000Z',
title: 'Àrea Andorra Difusió' title: 'Àrea Andorra Difusió'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
content: '<!DOCTYPE html><html><head></head><body></body></html>' content: '<!DOCTYPE html><html><head></head><body></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,108 +1,108 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos' const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos'
module.exports = { module.exports = {
site: 'anteltv.com.uy', site: 'anteltv.com.uy',
days: 2, days: 2,
async url({ date, channel }) { async url({ date, channel }) {
const session = await loadSessionDetails() const session = await loadSessionDetails()
if (!session || !session.token) return null if (!session || !session.token) return null
return `${API_ENDPOINT}/canales/epg/${ return `${API_ENDPOINT}/canales/epg/${
channel.site_id channel.site_id
}?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}` }?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}`
}, },
request: { request: {
async headers() { async headers() {
const session = await loadSessionDetails() const session = await loadSessionDetails()
if (!session || !session.jwt) return null if (!session || !session.jwt) return null
return { return {
authorization: `Bearer ${session.jwt}`, authorization: `Bearer ${session.jwt}`,
'x-frontend-id': 1196, 'x-frontend-id': 1196,
'x-service-id': 3, 'x-service-id': 3,
'x-system-id': 1 'x-system-id': 1
} }
} }
}, },
parser({ content }) { parser({ content }) {
let programs = [] let programs = []
let items = parseItems(content) let items = parseItems(content)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.nombre_programa, title: item.nombre_programa,
sub_title: item.subtitle, sub_title: item.subtitle,
description: item.descripcion_programa, description: item.descripcion_programa,
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const session = await loadSessionDetails() const session = await loadSessionDetails()
if (!session || !session.jwt || !session.token) return null if (!session || !session.jwt || !session.token) return null
const data = await axios const data = await axios
.get(`${API_ENDPOINT}/listas/68?token=${session.token}`, { .get(`${API_ENDPOINT}/listas/68?token=${session.token}`, {
headers: { headers: {
authorization: `Bearer ${session.jwt}`, authorization: `Bearer ${session.jwt}`,
'x-frontend-id': 1196, 'x-frontend-id': 1196,
'x-service-id': 3, 'x-service-id': 3,
'x-system-id': 1 'x-system-id': 1
} }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data.contenidos.map(c => { return data.contenidos.map(c => {
return { return {
lang: 'es', lang: 'es',
site_id: c.public_id, site_id: c.public_id,
name: c.nombre name: c.nombre
} }
}) })
} }
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo')
} }
function parseItems(content) { function parseItems(content) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.data)) return [] if (!data || !Array.isArray(data.data)) return []
return data.data return data.data
} }
function loadSessionDetails() { function loadSessionDetails() {
return axios return axios
.post( .post(
'https://veratv-be.vera.com.uy/api/sesiones', 'https://veratv-be.vera.com.uy/api/sesiones',
{ {
tipo: 'anonima' tipo: 'anonima'
}, },
{ {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
} }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
} }

View File

@@ -1,85 +1,85 @@
const { parser, url, request } = require('./anteltv.com.uy.config.js') const { parser, url, request } = require('./anteltv.com.uy.config.js')
const fs = require('fs') const fs = require('fs')
const axios = require('axios') const axios = require('axios')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
axios.post.mockImplementation((url, data, opts) => { axios.post.mockImplementation((url, data, opts) => {
if ( if (
url === 'https://veratv-be.vera.com.uy/api/sesiones' && url === 'https://veratv-be.vera.com.uy/api/sesiones' &&
JSON.stringify(opts.headers) === JSON.stringify(opts.headers) ===
JSON.stringify({ JSON.stringify({
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}) && }) &&
JSON.stringify(data) === JSON.stringify(data) ===
JSON.stringify({ JSON.stringify({
tipo: 'anonima' tipo: 'anonima'
}) })
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
}) })
} else { } else {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json')))
}) })
} }
}) })
const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '2s6nd', site_id: '2s6nd',
xmltv_id: 'Canal5.uy' xmltv_id: 'Canal5.uy'
} }
it('can generate valid url', async () => { it('can generate valid url', async () => {
const result = await url({ date, channel }) const result = await url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B' 'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B'
) )
}) })
it('can generate valid request headers', async () => { it('can generate valid request headers', async () => {
const result = await request.headers() const result = await request.headers()
expect(result).toMatchObject({ expect(result).toMatchObject({
authorization: authorization:
'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s',
'x-frontend-id': 1196, 'x-frontend-id': 1196,
'x-service-id': 3, 'x-service-id': 3,
'x-system-id': 1 'x-system-id': 1
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content }) let results = parser({ content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-02-11T02:30:00.000Z', start: '2023-02-11T02:30:00.000Z',
stop: '2023-02-11T04:00:00.000Z', stop: '2023-02-11T04:00:00.000Z',
title: 'Canal 5 Noticias rep.', title: 'Canal 5 Noticias rep.',
sub_title: '', sub_title: '',
description: '' description: ''
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,59 +1,59 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'antennaeurope.gr', site: 'antennaeurope.gr',
days: 2, days: 2,
url({ date }) { url({ date }) {
return `https://www.antennaeurope.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` return `https://www.antennaeurope.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.title').text().trim() return $item('.title').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('dt.col-time').clone().children().remove().end().text().trim() const time = $item('dt.col-time').clone().children().remove().end().text().trim()
return time return time
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
: null : null
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('dl.show').toArray() return $('dl.show').toArray()
} }

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./antennaeurope.gr.config.js') const { parser, url } = require('./antennaeurope.gr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe('https://www.antennaeurope.gr/el/tvguide.html?date=2025-01-21') expect(url({ date })).toBe('https://www.antennaeurope.gr/el/tvguide.html?date=2025-01-21')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(16) expect(results.length).toBe(16)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-21T03:45:00.000Z', start: '2025-01-21T03:45:00.000Z',
stop: '2025-01-21T07:50:00.000Z', stop: '2025-01-21T07:50:00.000Z',
title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ' title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ'
}) })
expect(results[15]).toMatchObject({ expect(results[15]).toMatchObject({
start: '2025-01-22T01:30:00.000Z', start: '2025-01-22T01:30:00.000Z',
stop: '2025-01-22T02:00:00.000Z', stop: '2025-01-22T02:00:00.000Z',
title: 'ΤΟ ΠΡΩΙΝΟ' title: 'ΤΟ ΠΡΩΙΝΟ'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,59 +1,59 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'antennapacific.gr', site: 'antennapacific.gr',
days: 2, days: 2,
url({ date }) { url({ date }) {
return `https://www.antennapacific.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` return `https://www.antennapacific.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.title').text().trim() return $item('.title').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('dt.col-time').clone().children().remove().end().text().trim() const time = $item('dt.col-time').clone().children().remove().end().text().trim()
return time return time
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
: null : null
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('dl.show').toArray() return $('dl.show').toArray()
} }

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./antennapacific.gr.config.js') const { parser, url } = require('./antennapacific.gr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe('https://www.antennapacific.gr/el/tvguide.html?date=2025-01-21') expect(url({ date })).toBe('https://www.antennapacific.gr/el/tvguide.html?date=2025-01-21')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(17) expect(results.length).toBe(17)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-21T05:00:00.000Z', start: '2025-01-21T05:00:00.000Z',
stop: '2025-01-21T06:00:00.000Z', stop: '2025-01-21T06:00:00.000Z',
title: 'ANT1 NEWS - ΚΕΝΤΡΙΚΟ ΔΕΛΤΙΟ' title: 'ANT1 NEWS - ΚΕΝΤΡΙΚΟ ΔΕΛΤΙΟ'
}) })
expect(results[16]).toMatchObject({ expect(results[16]).toMatchObject({
start: '2025-01-22T02:45:00.000Z', start: '2025-01-22T02:45:00.000Z',
stop: '2025-01-22T03:15:00.000Z', stop: '2025-01-22T03:15:00.000Z',
title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ' title: 'ΚΑΛΗΜΕΡΑ ΕΛΛΑΔΑ'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,59 +1,59 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'antennasatellite.gr', site: 'antennasatellite.gr',
days: 2, days: 2,
url({ date }) { url({ date }) {
return `https://www.antennasatellite.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}` return `https://www.antennasatellite.gr/el/tvguide.html?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.title').text().trim() return $item('.title').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('dt.col-time').clone().children().remove().end().text().trim() const time = $item('dt.col-time').clone().children().remove().end().text().trim()
return time return time
? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') ? dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens')
: null : null
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('dl.show').toArray() return $('dl.show').toArray()
} }

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./antennasatellite.gr.config.js') const { parser, url } = require('./antennasatellite.gr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe('https://www.antennasatellite.gr/el/tvguide.html?date=2025-01-21') expect(url({ date })).toBe('https://www.antennasatellite.gr/el/tvguide.html?date=2025-01-21')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(16) expect(results.length).toBe(16)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-21T04:00:00.000Z', start: '2025-01-21T04:00:00.000Z',
stop: '2025-01-21T04:40:00.000Z', stop: '2025-01-21T04:40:00.000Z',
title: 'ANT1 NEWS' title: 'ANT1 NEWS'
}) })
expect(results[15]).toMatchObject({ expect(results[15]).toMatchObject({
start: '2025-01-22T00:50:00.000Z', start: '2025-01-22T00:50:00.000Z',
stop: '2025-01-22T01:20:00.000Z', stop: '2025-01-22T01:20:00.000Z',
title: 'ΤΟ ΠΡΩΙΝΟ' title: 'ΤΟ ΠΡΩΙΝΟ'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,60 +1,60 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'arianatelevision.com', site: 'arianatelevision.com',
days: 2, days: 2,
url: 'https://www.arianatelevision.com/program-schedule/', url: 'https://www.arianatelevision.com/program-schedule/',
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart(item, date) let start = parseStart(item, date)
if (prev) { if (prev) {
if (start < prev.start) { if (start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.plus({ minutes: 30 }) const stop = start.plus({ minutes: 30 })
programs.push({ programs.push({
title: item.title, title: item.title,
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseStart(item, date) { function parseStart(item, date) {
const time = `${date.format('YYYY-MM-DD')} ${item.start}` const time = `${date.format('YYYY-MM-DD')} ${item.start}`
return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC() return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC()
} }
function parseItems(content, date) { function parseItems(content, date) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
const settings = $('#jtrt_table_settings_508').text() const settings = $('#jtrt_table_settings_508').text()
if (!settings) return [] if (!settings) return []
const data = JSON.parse(settings) const data = JSON.parse(settings)
if (!data || !Array.isArray(data)) return [] if (!data || !Array.isArray(data)) return []
let rows = data[0] let rows = data[0]
rows.shift() rows.shift()
const output = [] const output = []
rows.forEach(row => { rows.forEach(row => {
let day = date.day() + 2 let day = date.day() + 2
if (day > 7) day = 1 if (day > 7) day = 1
if (!row[0] || !row[day]) return if (!row[0] || !row[day]) return
output.push({ output.push({
start: row[0].trim(), start: row[0].trim(),
title: row[day].trim() title: row[day].trim()
}) })
}) })
return output return output
} }

View File

@@ -1,163 +1,163 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'arirang.com', site: 'arirang.com',
output: 'arirang.com.guide.xml', output: 'arirang.com.guide.xml',
channels: 'arirang.com.channels.xml', channels: 'arirang.com.channels.xml',
lang: 'en', lang: 'en',
days: 7, days: 7,
delay: 5000, delay: 5000,
url: 'https://www.arirang.com/v1.0/open/external/proxy', url: 'https://www.arirang.com/v1.0/open/external/proxy',
request: { request: {
method: 'POST', method: 'POST',
timeout: 5000, timeout: 5000,
cache: { ttl: 60 * 60 * 1000 }, cache: { ttl: 60 * 60 * 1000 },
headers: { headers: {
Accept: 'application/json, text/plain, */*', Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Origin: 'https://www.arirang.com', Origin: 'https://www.arirang.com',
Referer: 'https://www.arirang.com/schedule', Referer: 'https://www.arirang.com/schedule',
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
}, },
data: function (context) { data: function (context) {
const { channel, date } = context const { channel, date } = context
return { return {
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
method: 'POST', method: 'POST',
headers: {}, headers: {},
body: { body: {
data: { data: {
dmParam: { dmParam: {
chanId: channel.site_id, chanId: channel.site_id,
broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'), broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'),
planNo: '1' planNo: '1'
} }
} }
} }
} }
} }
}, },
logo: function (context) { logo: function (context) {
return context.channel.logo return context.channel.logo
}, },
async parser(context) { async parser(context) {
const programs = [] const programs = []
const items = parseItems(context.content) const items = parseItems(context.content)
for (let item of items) { for (let item of items) {
const programDetail = await parseProgramDetail(item) const programDetail = await parseProgramDetail(item)
programs.push({ programs.push({
title: parseTitle(programDetail), title: parseTitle(programDetail),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item), stop: parseStop(item),
image: parseImage(programDetail), image: parseImage(programDetail),
category: parseCategory(programDetail), category: parseCategory(programDetail),
description: parseDescription(programDetail) description: parseDescription(programDetail)
}) })
} }
return programs return programs
} }
} }
function parseItems(content) { function parseItems(content) {
if (content != '') { if (content != '') {
const data = JSON.parse(content) const data = JSON.parse(content)
return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek) return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek)
? [] ? []
: data.responseBody.dsSchWeek : data.responseBody.dsSchWeek
} else { } else {
return [] return []
} }
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul')
} }
function parseStop(item) { function parseStop(item) {
return dayjs return dayjs
.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') .tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul')
.add(item.broadRun, 'minute') .add(item.broadRun, 'minute')
} }
async function parseProgramDetail(item) { async function parseProgramDetail(item) {
return axios return axios
.post( .post(
'https://www.arirang.com/v1.0/open/program/detail', 'https://www.arirang.com/v1.0/open/program/detail',
{ {
bis_program_code: item.pgmCd bis_program_code: item.pgmCd
}, },
{ {
headers: { headers: {
Accept: 'application/json, text/plain, */*', Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Origin: 'https://www.arirang.com', Origin: 'https://www.arirang.com',
Referer: 'https://www.arirang.com/schedule', Referer: 'https://www.arirang.com/schedule',
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
}, },
timeout: 5000, timeout: 5000,
cache: { ttl: 60 * 1000 } cache: { ttl: 60 * 1000 }
} }
) )
.then(response => { .then(response => {
// console.log('Retrieved program detail: bis_program_code ' + item.pgmCd) // console.log('Retrieved program detail: bis_program_code ' + item.pgmCd)
return response.data return response.data
}) })
.catch(function () { .catch(function () {
// The provider/server may not have details on every single programs. // The provider/server may not have details on every single programs.
// console.log('Unavailable program detail: bis_program_code ' + item.pgmCd) // console.log('Unavailable program detail: bis_program_code ' + item.pgmCd)
}) })
} }
function parseTitle(programDetail) { function parseTitle(programDetail) {
if (programDetail && programDetail.title && programDetail.title[0] && programDetail.title[0].text) { if (programDetail && programDetail.title && programDetail.title[0] && programDetail.title[0].text) {
return programDetail.title[0].text return programDetail.title[0].text
} else { } else {
return '' return ''
} }
} }
function parseImage(programDetail) { function parseImage(programDetail) {
if (programDetail && programDetail.image && programDetail.image[0].url) { if (programDetail && programDetail.image && programDetail.image[0].url) {
return programDetail.image[0].url return programDetail.image[0].url
} else { } else {
return '' return ''
} }
} }
function parseCategory(programDetail) { function parseCategory(programDetail) {
if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) { if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) {
return programDetail.category_Info[0].title return programDetail.category_Info[0].title
} else { } else {
return '' return ''
} }
} }
function parseDescription(programDetail) { function parseDescription(programDetail) {
if ( if (
programDetail && programDetail &&
programDetail.content && programDetail.content &&
programDetail.content[0] && programDetail.content[0] &&
programDetail.content[0].text programDetail.content[0].text
) { ) {
let description = programDetail.content[0].text let description = programDetail.content[0].text
let regex = /(<([^>]+)>)/gi let regex = /(<([^>]+)>)/gi
return description.replace(regex, '') return description.replace(regex, '')
} else { } else {
return '' return ''
} }
} }

View File

@@ -1,72 +1,72 @@
const { url, parser } = require('./arirang.com.config.js') const { url, parser } = require('./arirang.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d') const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d')
const channel = { const channel = {
xmltv_id: 'ArirangWorld.kr', xmltv_id: 'ArirangWorld.kr',
site_id: 'CH_W', site_id: 'CH_W',
name: 'Arirang World', name: 'Arirang World',
lang: 'en', lang: 'en',
logo: 'https://i.imgur.com/5Aoithj.png' logo: 'https://i.imgur.com/5Aoithj.png'
} }
const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8')
const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8') const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8')
const context = { channel: channel, content: content, date: date } const context = { channel: channel, content: content, date: date }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy') expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy')
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
const results = await parser({ channel: channel, content: '', date: date }) const results = await parser({ channel: channel, content: '', date: date })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })
it('can parse response', async () => { it('can parse response', async () => {
axios.post.mockImplementation((url, data) => { axios.post.mockImplementation((url, data) => {
if ( if (
url === 'https://www.arirang.com/v1.0/open/external/proxy' && url === 'https://www.arirang.com/v1.0/open/external/proxy' &&
JSON.stringify(data) === JSON.stringify(data) ===
JSON.stringify({ JSON.stringify({
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
method: 'POST', method: 'POST',
headers: {}, headers: {},
body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } } body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } }
}) })
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(content) data: JSON.parse(content)
}) })
} else if ( } else if (
url === 'https://www.arirang.com/v1.0/open/program/detail' && url === 'https://www.arirang.com/v1.0/open/program/detail' &&
JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' }) JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' })
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(programDetail) data: JSON.parse(programDetail)
}) })
} else { } else {
return Promise.resolve({ return Promise.resolve({
data: '' data: ''
}) })
} }
}) })
const results = await parser(context) const results = await parser(context)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'Diplomat Archives: Hidden Stories', title: 'Diplomat Archives: Hidden Stories',
start: dayjs.tz(date, 'Asia/Seoul'), start: dayjs.tz(date, 'Asia/Seoul'),
stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'), stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'),
image: image:
'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202504/2985531324875408146.jpg', 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202504/2985531324875408146.jpg',
description: 'As of April 2025, S. Korea has established diplomatic relations with a total of 194 countries.\nAmong them are countries that have had ties and exchanges with Korea for hundreds of years.\nWith such long-standing relationships with so many nations,\nmight there be fascinating hidden stories between Korea and the rest of the world that we dont know yet? \n\n"Diplomats Archives: Hidden Stories" begins with this very question.\nTogether with foreign embassies in Korea, the series uncovers and sheds light on meaningful yet lesser-known stories between Korea and other countries.\nThrough this, we aim to reaffirm the deep friendships that have been built over time, highlight how countries are interconnected—bilaterally and multilaterally—\nand emphasize the importance of cooperation on the global stage today.', description: 'As of April 2025, S. Korea has established diplomatic relations with a total of 194 countries.\nAmong them are countries that have had ties and exchanges with Korea for hundreds of years.\nWith such long-standing relationships with so many nations,\nmight there be fascinating hidden stories between Korea and the rest of the world that we dont know yet? \n\n"Diplomats Archives: Hidden Stories" begins with this very question.\nTogether with foreign embassies in Korea, the series uncovers and sheds light on meaningful yet lesser-known stories between Korea and other countries.\nThrough this, we aim to reaffirm the deep friendships that have been built over time, highlight how countries are interconnected—bilaterally and multilaterally—\nand emphasize the importance of cooperation on the global stage today.',
category: 'Current Affairs' category: 'Current Affairs'
}) })
}) })

View File

@@ -1,70 +1,70 @@
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const dayjs = require('dayjs') const dayjs = require('dayjs')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'artonline.tv', site: 'artonline.tv',
days: 2, days: 2,
url: function ({ channel }) { url: function ({ channel }) {
const [, site_id] = channel.site_id.split('#') const [, site_id] = channel.site_id.split('#')
return `https://www.artonline.tv/Home/Tvlist${site_id}` return `https://www.artonline.tv/Home/Tvlist${site_id}`
}, },
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'content-type': 'application/x-www-form-urlencoded' 'content-type': 'application/x-www-form-urlencoded'
}, },
data: function ({ date }) { data: function ({ date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd') const diff = date.diff(dayjs.utc().startOf('d'), 'd')
const params = new URLSearchParams() const params = new URLSearchParams()
params.append('objId', diff) params.append('objId', diff)
return params return params
} }
}, },
parser: function ({ content }) { parser: function ({ content }) {
const programs = [] const programs = []
if (!content) return programs if (!content) return programs
const items = JSON.parse(content) const items = JSON.parse(content)
items.forEach(item => { items.forEach(item => {
const image = parseImage(item) const image = parseImage(item)
const start = parseStart(item) const start = parseStart(item)
const duration = parseDuration(item) const duration = parseDuration(item)
const stop = start.add(duration, 's') const stop = start.add(duration, 's')
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description, description: item.description,
image, image,
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseStart(item) { function parseStart(item) {
const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /) const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /)
const [HH, mm] = item.start_Time.split(':') const [HH, mm] = item.start_Time.split(':')
return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh') return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh')
} }
function parseDuration(item) { function parseDuration(item) {
const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/) const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/)
return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss) return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss)
} }
function parseImage(item) { function parseImage(item) {
return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null
} }

View File

@@ -1,86 +1,86 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'awilime.com', site: 'awilime.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://www.awilime.com/tv/napi_musor/${channel.site_id}/${date.format('YYYY_MM_DD')}` return `https://www.awilime.com/tv/napi_musor/${channel.site_id}/${date.format('YYYY_MM_DD')}`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
if (!start) return if (!start) return
if (prev) { if (prev) {
prev.stop = start prev.stop = start
} }
const stop = start.plus({ minute: 30 }) const stop = start.plus({ minute: 30 })
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
sub_title: parseSubTitle($item), sub_title: parseSubTitle($item),
description: parseDescription($item), description: parseDescription($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const html = await axios const html = await axios
.get('https://www.awilime.com/tv/napi_musor') .get('https://www.awilime.com/tv/napi_musor')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const items = $('#body > div.tk > div > div').toArray() const items = $('#body > div.tk > div > div').toArray()
const channels = [] const channels = []
items.forEach(item => { items.forEach(item => {
const name = $(item).find('a').text().trim() const name = $(item).find('a').text().trim()
const url = $(item).find('a').attr('href') const url = $(item).find('a').attr('href')
const [, site_id] = url.match(/\/tv\/napi_musor\/(.*)/) || [null, null] const [, site_id] = url.match(/\/tv\/napi_musor\/(.*)/) || [null, null]
if (!site_id) return if (!site_id) return
if (channels.find(channel => channel.site_id === site_id)) return if (channels.find(channel => channel.site_id === site_id)) return
channels.push({ channels.push({
lang: 'hu', lang: 'hu',
site_id, site_id,
name name
}) })
}) })
return channels return channels
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('b > a').text().trim() return $item('b > a').text().trim()
} }
function parseSubTitle($item) { function parseSubTitle($item) {
return $item('i').clone().children().remove('s').end().text().trim() return $item('i').clone().children().remove('s').end().text().trim()
} }
function parseDescription($item) { function parseDescription($item) {
return $item('p').text().trim() return $item('p').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
let time = $item('b').clone().children().remove().end().text().trim() let time = $item('b').clone().children().remove().end().text().trim()
if (!time || !/^\d/.test(time)) return null if (!time || !/^\d/.test(time)) return null
time = `${date.format('YYYY-MM-DD')} ${time}` time = `${date.format('YYYY-MM-DD')} ${time}`
return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Budapest' }).toUTC() return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Budapest' }).toUTC()
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('#body > div.tdc > div.td2 > div').toArray() return $('#body > div.tdc > div.td2 > div').toArray()
} }

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./awilime.com.config.js') const { parser, url } = require('./awilime.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2024-06-26', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-06-26', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'budapest_europa_tv', site_id: 'budapest_europa_tv',
xmltv_id: 'BudapestEuropaTelevizio.hu' xmltv_id: 'BudapestEuropaTelevizio.hu'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://www.awilime.com/tv/napi_musor/budapest_europa_tv/2024_06_26' 'https://www.awilime.com/tv/napi_musor/budapest_europa_tv/2024_06_26'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => { const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(15) expect(results.length).toBe(15)
expect(results[3]).toMatchObject({ expect(results[3]).toMatchObject({
start: '2024-06-26T07:00:00.000Z', start: '2024-06-26T07:00:00.000Z',
stop: '2024-06-26T08:00:00.000Z', stop: '2024-06-26T08:00:00.000Z',
title: 'Ébredés', title: 'Ébredés',
sub_title: 'Amerikai dokumentumfilm (2018)', sub_title: 'Amerikai dokumentumfilm (2018)',
description: 'Balla Tibor misszionárius' description: 'Balla Tibor misszionárius'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: content:
'<html><head><title>Object moved</title></head><body><h2>Object moved to <a href="/tv/napi_musor/budapest_europa_tv/2024_06_24">here</a>.</h2></body></html>' '<html><head><title>Object moved</title></head><body><h2>Object moved to <a href="/tv/napi_musor/budapest_europa_tv/2024_06_24">here</a>.</h2></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,110 +1,110 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'bein.com', site: 'bein.com',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url: function ({ date, channel }) { url: function ({ date, channel }) {
const [category] = channel.site_id.split('#') const [category] = channel.site_id.split('#')
const postid = channel.lang === 'ar' ? '25344' : '25356' const postid = channel.lang === 'ar' ? '25344' : '25356'
return `https://www.bein.com/${ return `https://www.bein.com/${
channel.lang channel.lang
}/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format( }/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net` )}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net`
}, },
parser: function ({ content, channel, date }) { parser: function ({ content, channel, date }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 }) date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 })
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const title = parseTitle($item) const title = parseTitle($item)
if (!title) return if (!title) return
const category = parseCategory($item) const category = parseCategory($item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseTime($item, date) let start = parseTime($item, date)
if (prev) { if (prev) {
if (start < prev.start) { if (start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
date = date.plus({ days: 1 }) date = date.plus({ days: 1 })
} }
prev.stop = start prev.stop = start
} }
let stop = parseTime($item, start) let stop = parseTime($item, start)
if (stop < start) { if (stop < start) {
stop = stop.plus({ days: 1 }) stop = stop.plus({ days: 1 })
} }
programs.push({ programs.push({
title, title,
category, category,
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels({ lang }) { async channels({ lang }) {
const categories = ['entertainment', 'sports'] const categories = ['entertainment', 'sports']
let channels = [] let channels = []
for (let category of categories) { for (let category of categories) {
const url = `https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&offset=0&category=${category}&serviceidentity=bein.net&mins=00&cdate=${dayjs().format( const url = `https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&offset=0&category=${category}&serviceidentity=bein.net&mins=00&cdate=${dayjs().format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&language=${lang.toUpperCase()}&postid=25356&loadindex=0` )}&language=${lang.toUpperCase()}&postid=25356&loadindex=0`
const data = await axios const data = await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(data) const $ = cheerio.load(data)
$('.container-tvguide > div').each((i, el) => { $('.container-tvguide > div').each((i, el) => {
const id = $(el).attr('id') const id = $(el).attr('id')
if (!id || !/^channels_\d+/.test(id)) return if (!id || !/^channels_\d+/.test(id)) return
const [, channelId] = id.split('_') const [, channelId] = id.split('_')
channels.push({ channels.push({
lang, lang,
site_id: `${category}#${channelId}`, site_id: `${category}#${channelId}`,
name: channelId name: channelId
}) })
}) })
} }
return channels return channels
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.title').text() return $item('.title').text()
} }
function parseCategory($item) { function parseCategory($item) {
return $item('.format').text() return $item('.format').text()
} }
function parseTime($item, date) { function parseTime($item, date) {
let [, time] = $item('.time') let [, time] = $item('.time')
.text() .text()
.match(/^(\d{2}:\d{2})/) || [null, null] .match(/^(\d{2}:\d{2})/) || [null, null]
if (!time) return null if (!time) return null
time = `${date.toFormat('yyyy-MM-dd')} ${time}` time = `${date.toFormat('yyyy-MM-dd')} ${time}`
return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC() return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC()
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray()
} }

View File

@@ -1,58 +1,58 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const { parser, url } = require('./bein.com.config.js') const { parser, url } = require('./bein.com.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' } const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net' 'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html')) const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html'))
const results = parser({ date, channel, content }).map(p => { const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-01-18T20:15:00.000Z', start: '2023-01-18T20:15:00.000Z',
stop: '2023-01-18T22:15:00.000Z', stop: '2023-01-18T22:15:00.000Z',
title: 'The Walk', title: 'The Walk',
category: 'Movies' category: 'Movies'
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
start: '2023-01-18T22:15:00.000Z', start: '2023-01-18T22:15:00.000Z',
stop: '2023-01-19T00:00:00.000Z', stop: '2023-01-19T00:00:00.000Z',
title: 'Resident Evil: Welcome To Raccoon City', title: 'Resident Evil: Welcome To Raccoon City',
category: 'Movies' category: 'Movies'
}) })
expect(results[10]).toMatchObject({ expect(results[10]).toMatchObject({
start: '2023-01-19T15:30:00.000Z', start: '2023-01-19T15:30:00.000Z',
stop: '2023-01-19T18:00:00.000Z', stop: '2023-01-19T18:00:00.000Z',
title: 'Spider-Man: No Way Home', title: 'Spider-Man: No Way Home',
category: 'Movies' category: 'Movies'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html')) const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html'))
const result = parser({ const result = parser({
date, date,
channel, channel,
content: noContent content: noContent
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,73 +1,73 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'beinsports.com', site: 'beinsports.com',
days: 2, days: 2,
request: { request: {
headers: { headers: {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
} }
}, },
url: function ({ date, channel }) { url: function ({ date, channel }) {
return `https://www.beinsports.com/api/opta/tv-event?&startBefore=${date return `https://www.beinsports.com/api/opta/tv-event?&startBefore=${date
.add(1, 'd') .add(1, 'd')
.format('YYYY-MM-DDTHH:mm:ss.SSS')}Z&endAfter=${date.format( .format('YYYY-MM-DDTHH:mm:ss.SSS')}Z&endAfter=${date.format(
'YYYY-MM-DDTHH:mm:ss.SSS' 'YYYY-MM-DDTHH:mm:ss.SSS'
)}Z&channelIds=${channel.site_id}` )}Z&channelIds=${channel.site_id}`
}, },
parser: function ({ content }) { parser: function ({ content }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
if (!items.length == 0) { if (!items.length == 0) {
items.forEach(item => { items.forEach(item => {
const start = dayjs.utc(item.startDate) const start = dayjs.utc(item.startDate)
const stop = dayjs.utc(item.endDate) const stop = dayjs.utc(item.endDate)
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description, description: item.description,
start, start,
stop stop
}) })
}) })
} }
return programs return programs
}, },
async channels({ region, lang }) { async channels({ region, lang }) {
const data = await axios const data = await axios
.get(`https://www.beinsports.com/api/opta/tv-channel?region=${lang}-${region}`, this.request) .get(`https://www.beinsports.com/api/opta/tv-channel?region=${lang}-${region}`, this.request)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.rows.map(item => { return data.rows.map(item => {
return { return {
lang, lang,
site_id: item.id, site_id: item.id,
name: item.name name: item.name
} }
}) })
} }
} }
function parseItems(content) { function parseItems(content) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch { } catch {
return [] return []
} }
if (!data || !data['rows']) { if (!data || !data['rows']) {
return [] return []
} }
return data.rows return data.rows
} }

View File

@@ -1,43 +1,43 @@
const { parser, url } = require('./beinsports.com.config.js') const { parser, url } = require('./beinsports.com.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-10-22T00:00:00.000', '"YYYY-MM-DDTHH:mm:ss.SSS').startOf('d') const date = dayjs.utc('2023-10-22T00:00:00.000', '"YYYY-MM-DDTHH:mm:ss.SSS').startOf('d')
const channel = { site_id: 'C244C48D-3B54-406A-94C9-D63B16318267', xmltv_id: 'beINSportsUSA.us' } const channel = { site_id: 'C244C48D-3B54-406A-94C9-D63B16318267', xmltv_id: 'beINSportsUSA.us' }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://www.beinsports.com/api/opta/tv-event?&startBefore=2023-10-23T00:00:00.000Z&endAfter=2023-10-22T00:00:00.000Z&channelIds=C244C48D-3B54-406A-94C9-D63B16318267' 'https://www.beinsports.com/api/opta/tv-event?&startBefore=2023-10-23T00:00:00.000Z&endAfter=2023-10-22T00:00:00.000Z&channelIds=C244C48D-3B54-406A-94C9-D63B16318267'
) )
}) })
const content = const content =
'{"count":1,"rows":[{"data":{"eventId":"2028126","eventDate":"2023-10-21T10:30:00","utcEventDate":"2023-10-20T23:30:00","duration":"90","programId":"106230","programTypeId":"5","title":"ATP 500"},"duration":5400000,"title":"Tokyo Day 5 QF 2","startDate":"2023-10-20T23:30:00.000Z","endDate":"2023-10-21T01:00:00.000Z","description":"Exclusive coverage of the 2023 ATP Tour on beIN SPORTS","channelId":"164C0EDA-EBCE-4AA6-9DDA-D603E0948B9F"}]}' '{"count":1,"rows":[{"data":{"eventId":"2028126","eventDate":"2023-10-21T10:30:00","utcEventDate":"2023-10-20T23:30:00","duration":"90","programId":"106230","programTypeId":"5","title":"ATP 500"},"duration":5400000,"title":"Tokyo Day 5 QF 2","startDate":"2023-10-20T23:30:00.000Z","endDate":"2023-10-21T01:00:00.000Z","description":"Exclusive coverage of the 2023 ATP Tour on beIN SPORTS","channelId":"164C0EDA-EBCE-4AA6-9DDA-D603E0948B9F"}]}'
it('can parse response', () => { it('can parse response', () => {
const result = parser({ content, channel, date }).map(p => { const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2023-10-20T23:30:00.000Z', start: '2023-10-20T23:30:00.000Z',
stop: '2023-10-21T01:00:00.000Z', stop: '2023-10-21T01:00:00.000Z',
title: 'Tokyo Day 5 QF 2', title: 'Tokyo Day 5 QF 2',
description: 'Exclusive coverage of the 2023 ATP Tour on beIN SPORTS' description: 'Exclusive coverage of the 2023 ATP Tour on beIN SPORTS'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '[]' content: '[]'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,93 +1,93 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.Ls.en.weekStart = 1 dayjs.Ls.en.weekStart = 1
module.exports = { module.exports = {
site: 'berrymedia.co.kr', site: 'berrymedia.co.kr',
days: 2, days: 2,
url({ channel }) { url({ channel }) {
return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php` return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php`
}, },
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
}, },
data({ date }) { data({ date }) {
let params = new URLSearchParams() let params = new URLSearchParams()
let startOfWeek = date.startOf('week').format('YYYY-MM-DD') let startOfWeek = date.startOf('week').format('YYYY-MM-DD')
let endOfWeek = date.endOf('week').format('YYYY-MM-DD') let endOfWeek = date.endOf('week').format('YYYY-MM-DD')
params.append('week', `${startOfWeek}~${endOfWeek}`) params.append('week', `${startOfWeek}~${endOfWeek}`)
params.append('day', date.format('YYYY-MM-DD')) params.append('day', date.format('YYYY-MM-DD'))
return params return params
} }
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
category: parseCategory($item), category: parseCategory($item),
rating: parseRating($item), rating: parseRating($item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('span:nth-child(1)').text().trim() const time = $item('span:nth-child(1)').text().trim()
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul')
} }
function parseTitle($item) { function parseTitle($item) {
return $item('span.sdfsdf').clone().children().remove().end().text().trim() return $item('span.sdfsdf').clone().children().remove().end().text().trim()
} }
function parseCategory($item) { function parseCategory($item) {
return $item('span:nth-child(2) > p').text().trim() return $item('span:nth-child(2) > p').text().trim()
} }
function parseRating($item) { function parseRating($item) {
const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim() const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim()
return rating return rating
? { ? {
system: 'KMRB', system: 'KMRB',
value: rating value: rating
} }
: null : null
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('.sc_time dd').toArray() return $('.sc_time dd').toArray()
} }

View File

@@ -1,77 +1,77 @@
const { parser, url, request } = require('./berrymedia.co.kr.config.js') const { parser, url, request } = require('./berrymedia.co.kr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-01-26', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-01-26', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '', site_id: '',
xmltv_id: 'GTV.kr' xmltv_id: 'GTV.kr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php') expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php')
}) })
it('can generate request method', () => { it('can generate request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
let params = request.data({ date }) let params = request.data({ date })
expect(params.get('week')).toBe('2023-01-23~2023-01-29') expect(params.get('week')).toBe('2023-01-23~2023-01-29')
expect(params.get('day')).toBe('2023-01-26') expect(params.get('day')).toBe('2023-01-26')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-01-25T15:00:00.000Z', start: '2023-01-25T15:00:00.000Z',
stop: '2023-01-25T16:00:00.000Z', stop: '2023-01-25T16:00:00.000Z',
title: '더트롯쇼', title: '더트롯쇼',
category: '연예/오락', category: '연예/오락',
rating: { rating: {
system: 'KMRB', system: 'KMRB',
value: '15' value: '15'
} }
}) })
expect(results[17]).toMatchObject({ expect(results[17]).toMatchObject({
start: '2023-01-26T13:50:00.000Z', start: '2023-01-26T13:50:00.000Z',
stop: '2023-01-26T14:20:00.000Z', stop: '2023-01-26T14:20:00.000Z',
title: '나는 자연인이다', title: '나는 자연인이다',
category: '교양', category: '교양',
rating: { rating: {
system: 'KMRB', system: 'KMRB',
value: 'ALL' value: 'ALL'
} }
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,133 +1,133 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.reportv.com.ar/finder' const API_ENDPOINT = 'https://www.reportv.com.ar/finder'
module.exports = { module.exports = {
site: 'cableplus.com.uy', site: 'cableplus.com.uy',
days: 2, days: 2,
url: `${API_ENDPOINT}/channel`, url: `${API_ENDPOINT}/channel`,
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}, },
data({ date, channel }) { data({ date, channel }) {
const params = new URLSearchParams() const params = new URLSearchParams()
params.append('idAlineacion', '3017') params.append('idAlineacion', '3017')
params.append('idSenial', channel.site_id) params.append('idSenial', channel.site_id)
params.append('fecha', date.format('YYYY-MM-DD')) params.append('fecha', date.format('YYYY-MM-DD'))
params.append('hora', '00:00') params.append('hora', '00:00')
return params return params
} }
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
categories: parseCategories($item), categories: parseCategories($item),
image: parseImage($item), image: parseImage($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const params = new URLSearchParams({ idAlineacion: '3017' }) const params = new URLSearchParams({ idAlineacion: '3017' })
const data = await axios const data = await axios
.post(`${API_ENDPOINT}/channelGrid`, params, { .post(`${API_ENDPOINT}/channelGrid`, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
const $ = cheerio.load(data) const $ = cheerio.load(data)
return $('.senial') return $('.senial')
.map(function () { .map(function () {
return { return {
lang: 'es', lang: 'es',
site_id: $(this).attr('id'), site_id: $(this).attr('id'),
name: $(this).find('img').attr('alt') name: $(this).find('img').attr('alt')
} }
}) })
.get() .get()
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child')
.text() .text()
.trim() .trim()
} }
function parseImage($item) { function parseImage($item) {
return $item('img').data('src') || $item('img').attr('src') || null return $item('img').data('src') || $item('img').attr('src') || null
} }
function parseCategories($item) { function parseCategories($item) {
return $item('p.evento_genero') return $item('p.evento_genero')
.map(function () { .map(function () {
return $item(this).text().trim() return $item(this).text().trim()
}) })
.toArray() .toArray()
} }
function parseStart($item, date) { function parseStart($item, date) {
let time = $item('.grid_fecha_hora').text().trim() let time = $item('.grid_fecha_hora').text().trim()
if (time) { if (time) {
return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo')
} }
time = $item('.fechaHora').text().trim() time = $item('.fechaHora').text().trim()
return time return time
? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo')
: null : null
} }
function parseItems(content, date) { function parseItems(content, date) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa')
.filter(function () { .filter(function () {
return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1
}) })
.toArray() .toArray()
let otherItems = $('#owl-pc > .item-program') let otherItems = $('#owl-pc > .item-program')
.filter(function () { .filter(function () {
return ( return (
$(this) $(this)
.find('.evento_titulo > .horario > p.fechaHora') .find('.evento_titulo > .horario > p.fechaHora')
.text() .text()
.indexOf(date.format('DD/MM')) > -1 .indexOf(date.format('DD/MM')) > -1
) )
}) })
.toArray() .toArray()
return featuredItems.concat(otherItems) return featuredItems.concat(otherItems)
} }

View File

@@ -1,73 +1,73 @@
const { parser, url, request } = require('./cableplus.com.uy.config.js') const { parser, url, request } = require('./cableplus.com.uy.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '2035', site_id: '2035',
xmltv_id: 'APlusV.uy' xmltv_id: 'APlusV.uy'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://www.reportv.com.ar/finder/channel') expect(url).toBe('https://www.reportv.com.ar/finder/channel')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const params = request.data({ date, channel }) const params = request.data({ date, channel })
expect(params.get('idAlineacion')).toBe('3017') expect(params.get('idAlineacion')).toBe('3017')
expect(params.get('idSenial')).toBe('2035') expect(params.get('idSenial')).toBe('2035')
expect(params.get('fecha')).toBe('2023-02-12') expect(params.get('fecha')).toBe('2023-02-12')
expect(params.get('hora')).toBe('00:00') expect(params.get('hora')).toBe('00:00')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(21) expect(results.length).toBe(21)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-02-12T09:30:00.000Z', start: '2023-02-12T09:30:00.000Z',
stop: '2023-02-12T10:30:00.000Z', stop: '2023-02-12T10:30:00.000Z',
title: 'Revista agropecuaria', title: 'Revista agropecuaria',
image: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', image: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg',
categories: [] categories: []
}) })
expect(results[4]).toMatchObject({ expect(results[4]).toMatchObject({
start: '2023-02-12T12:30:00.000Z', start: '2023-02-12T12:30:00.000Z',
stop: '2023-02-12T13:30:00.000Z', stop: '2023-02-12T13:30:00.000Z',
title: 'De pago en pago', title: 'De pago en pago',
image: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', image: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg',
categories: ['Cultural'] categories: ['Cultural']
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,197 +1,197 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'canalplus.com', site: 'canalplus.com',
days: 2, days: 2,
url: async function ({ channel, date }) { url: async function ({ channel, date }) {
const [region, site_id] = channel.site_id.split('#') const [region, site_id] = channel.site_id.split('#')
const baseUrl = const baseUrl =
region === 'pl' region === 'pl'
? 'https://www.canalplus.com/pl/program-tv/' ? 'https://www.canalplus.com/pl/program-tv/'
: `https://www.canalplus.com/${region}/programme-tv/` : `https://www.canalplus.com/${region}/programme-tv/`
const data = await axios const data = await axios
.get(baseUrl) .get(baseUrl)
.then(r => r.data.toString()) .then(r => r.data.toString())
.catch(err => console.log(err)) .catch(err => console.log(err))
const token = parseToken(data) const token = parseToken(data)
const path = region === 'pl' ? 'mycanalint' : 'mycanal' const path = region === 'pl' ? 'mycanalint' : 'mycanal'
const diff = date.diff(dayjs.utc().startOf('d'), 'd') const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://hodor.canalplus.pro/api/v2/${path}/channels/${token}/${site_id}/broadcasts/day/${diff}` return `https://hodor.canalplus.pro/api/v2/${path}/channels/${token}/${site_id}/broadcasts/day/${diff}`
}, },
async parser({ content }) { async parser({ content }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
for (let item of items) { for (let item of items) {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const details = await loadProgramDetails(item) const details = await loadProgramDetails(item)
const info = parseInfo(details) const info = parseInfo(details)
const start = parseStart(item) const start = parseStart(item)
if (prev) prev.stop = start if (prev) prev.stop = start
const stop = start.add(1, 'h') const stop = start.add(1, 'h')
programs.push({ programs.push({
title: item.title, title: item.title,
description: parseDescription(info), description: parseDescription(info),
image: parseImage(info), image: parseImage(info),
actors: parseCast(info, 'Avec :'), actors: parseCast(info, 'Avec :'),
director: parseCast(info, 'De :'), director: parseCast(info, 'De :'),
writer: parseCast(info, 'Scénario :'), writer: parseCast(info, 'Scénario :'),
composer: parseCast(info, 'Musique :'), composer: parseCast(info, 'Musique :'),
presenter: parseCast(info, 'Présenté par :'), presenter: parseCast(info, 'Présenté par :'),
date: parseDate(info), date: parseDate(info),
rating: parseRating(info), rating: parseRating(info),
start, start,
stop stop
}) })
} }
return programs return programs
}, },
async channels({ country }) { async channels({ country }) {
const paths = { const paths = {
ad: 'cpafr/ad', ad: 'cpafr/ad',
bf: 'cpafr/bf', bf: 'cpafr/bf',
bi: 'cpafr/bi', bi: 'cpafr/bi',
bj: 'cpafr/bj', bj: 'cpafr/bj',
bl: 'cpant/bl', bl: 'cpant/bl',
cd: 'cpafr/cd', cd: 'cpafr/cd',
cf: 'cpafr/cf', cf: 'cpafr/cf',
cg: 'cpafr/cg', cg: 'cpafr/cg',
ch: 'cpche', ch: 'cpche',
ci: 'cpafr/ci', ci: 'cpafr/ci',
cm: 'cpafr/cm', cm: 'cpafr/cm',
cv: 'cpafr/cv', cv: 'cpafr/cv',
dj: 'cpafr/dj', dj: 'cpafr/dj',
fr: 'cpfra', fr: 'cpfra',
ga: 'cpafr/ga', ga: 'cpafr/ga',
gf: 'cpant/gf', gf: 'cpant/gf',
gh: 'cpafr/gh', gh: 'cpafr/gh',
gm: 'cpafr/gm', gm: 'cpafr/gm',
gn: 'cpafr/gn', gn: 'cpafr/gn',
gp: 'cpafr/gp', gp: 'cpafr/gp',
gw: 'cpafr/gw', gw: 'cpafr/gw',
ht: 'cpant/ht', ht: 'cpant/ht',
mf: 'cpant/mf', mf: 'cpant/mf',
mg: 'cpafr/mg', mg: 'cpafr/mg',
ml: 'cpafr/ml', ml: 'cpafr/ml',
mq: 'cpant/mq', mq: 'cpant/mq',
mr: 'cpafr/mr', mr: 'cpafr/mr',
mu: 'cpmus/mu', mu: 'cpmus/mu',
nc: 'cpncl/nc', nc: 'cpncl/nc',
ne: 'cpafr/ne', ne: 'cpafr/ne',
pf: 'cppyf/pf', pf: 'cppyf/pf',
pl: 'cppol', pl: 'cppol',
re: 'cpreu/re', re: 'cpreu/re',
rw: 'cpafr/rw', rw: 'cpafr/rw',
sl: 'cpafr/sl', sl: 'cpafr/sl',
sn: 'cpafr/sn', sn: 'cpafr/sn',
td: 'cpafr/td', td: 'cpafr/td',
tg: 'cpafr/tg', tg: 'cpafr/tg',
wf: 'cpncl/wf', wf: 'cpncl/wf',
yt: 'cpreu/yt' yt: 'cpreu/yt'
} }
let channels = [] let channels = []
const path = paths[country] const path = paths[country]
const url = `https://secure-webtv-static.canal-plus.com/metadata/${path}/all/v2.2/globalchannels.json` const url = `https://secure-webtv-static.canal-plus.com/metadata/${path}/all/v2.2/globalchannels.json`
const data = await axios const data = await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
data.channels.forEach(channel => { data.channels.forEach(channel => {
const site_id = country === 'fr' ? `#${channel.id}` : `${country}#${channel.id}` const site_id = country === 'fr' ? `#${channel.id}` : `${country}#${channel.id}`
if (channel.name === '.') return if (channel.name === '.') return
channels.push({ channels.push({
lang: 'fr', lang: 'fr',
site_id, site_id,
name: channel.name name: channel.name
}) })
}) })
return channels return channels
} }
} }
function parseToken(data) { function parseToken(data) {
const [, token] = data.match(/"token":"([^"]+)/) || [null, null] const [, token] = data.match(/"token":"([^"]+)/) || [null, null]
return token return token
} }
function parseStart(item) { function parseStart(item) {
return item && item.startTime ? dayjs(item.startTime) : null return item && item.startTime ? dayjs(item.startTime) : null
} }
function parseImage(info) { function parseImage(info) {
return info ? info.URLImage : null return info ? info.URLImage : null
} }
function parseDescription(info) { function parseDescription(info) {
return info ? info.summary : null return info ? info.summary : null
} }
function parseInfo(data) { function parseInfo(data) {
if (!data || !data.detail || !data.detail.informations) return null if (!data || !data.detail || !data.detail.informations) return null
return data.detail.informations return data.detail.informations
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
if (!item.onClick || !item.onClick.URLPage) return {} if (!item.onClick || !item.onClick.URLPage) return {}
return await axios return await axios
.get(item.onClick.URLPage) .get(item.onClick.URLPage)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
} }
function parseItems(content) { function parseItems(content) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.timeSlices)) return [] if (!data || !Array.isArray(data.timeSlices)) return []
return data.timeSlices.reduce((acc, curr) => { return data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents) acc = acc.concat(curr.contents)
return acc return acc
}, []) }, [])
} }
function parseCast(info, type) { function parseCast(info, type) {
let people = [] let people = []
if (info && info.personnalities) { if (info && info.personnalities) {
const personnalities = info.personnalities.find(i => i.prefix == type) const personnalities = info.personnalities.find(i => i.prefix == type)
if (!personnalities) return people if (!personnalities) return people
for (let person of personnalities.personnalitiesList) { for (let person of personnalities.personnalitiesList) {
people.push(person.title) people.push(person.title)
} }
} }
return people return people
} }
function parseDate(info) { function parseDate(info) {
return info && info.productionYear ? info.productionYear : null return info && info.productionYear ? info.productionYear : null
} }
function parseRating(info) { function parseRating(info) {
if (!info || !info.parentalRatings) return null if (!info || !info.parentalRatings) return null
let rating = info.parentalRatings.find(i => i.authority === 'CSA') let rating = info.parentalRatings.find(i => i.authority === 'CSA')
if (!rating || Array.isArray(rating)) return null if (!rating || Array.isArray(rating)) return null
if (rating.value === '1') return null if (rating.value === '1') return null
if (rating.value === '2') rating.value = '-10' if (rating.value === '2') rating.value = '-10'
if (rating.value === '3') rating.value = '-12' if (rating.value === '3') rating.value = '-12'
if (rating.value === '4') rating.value = '-16' if (rating.value === '4') rating.value = '-16'
if (rating.value === '5') rating.value = '-18' if (rating.value === '5') rating.value = '-18'
return { return {
system: rating.authority, system: rating.authority,
value: rating.value value: rating.value
} }
} }

View File

@@ -1,146 +1,146 @@
const { parser, url } = require('./canalplus.com.config.js') const { parser, url } = require('./canalplus.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const channel = { const channel = {
site_id: 'bi#198', site_id: 'bi#198',
xmltv_id: 'CanalPlusCinemaFrance.fr' xmltv_id: 'CanalPlusCinemaFrance.fr'
} }
it('can generate valid url for today', done => { it('can generate valid url for today', done => {
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') { if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
const today = dayjs.utc().startOf('d') const today = dayjs.utc().startOf('d')
url({ channel, date: today }) url({ channel, date: today })
.then(result => { .then(result => {
expect(result).toBe( expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0'
) )
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can generate valid url for tomorrow', done => { it('can generate valid url for tomorrow', done => {
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') { if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
const tomorrow = dayjs.utc().startOf('d').add(1, 'd') const tomorrow = dayjs.utc().startOf('d').add(1, 'd')
url({ channel, date: tomorrow }) url({ channel, date: tomorrow })
.then(result => { .then(result => {
expect(result).toBe( expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1'
) )
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can parse response', done => { it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if ( if (
url === url ===
'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true' 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
}) })
} else if ( } else if (
url === url ===
'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content }) parser({ content })
.then(result => { .then(result => {
result.map(p => { result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2023-01-12T06:28:00.000Z', start: '2023-01-12T06:28:00.000Z',
stop: '2023-01-12T12:06:00.000Z', stop: '2023-01-12T12:06:00.000Z',
title: 'Le cercle', title: 'Le cercle',
description: description:
"Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.", "Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.",
image: image:
'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573', 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573',
presenter: ['Lily Bloom'], presenter: ['Lily Bloom'],
rating: { rating: {
system: 'CSA', system: 'CSA',
value: '-10' value: '-10'
} }
}, },
{ {
start: '2023-01-12T12:06:00.000Z', start: '2023-01-12T12:06:00.000Z',
stop: '2023-01-12T13:06:00.000Z', stop: '2023-01-12T13:06:00.000Z',
title: 'Illusions perdues', title: 'Illusions perdues',
description: description:
"Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...", "Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...",
image: image:
'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485', 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485',
director: ['Xavier Giannoli'], director: ['Xavier Giannoli'],
actors: [ actors: [
'Benjamin Voisin', 'Benjamin Voisin',
'Cécile de France', 'Cécile de France',
'Vincent Lacoste', 'Vincent Lacoste',
'Xavier Dolan', 'Xavier Dolan',
'Gérard Depardieu', 'Gérard Depardieu',
'Salomé Dewaels', 'Salomé Dewaels',
'Jeanne Balibar', 'Jeanne Balibar',
'Louis-Do de Lencquesaing', 'Louis-Do de Lencquesaing',
'Alexis Barbosa', 'Alexis Barbosa',
'Jean-François Stévenin', 'Jean-François Stévenin',
'André Marcon', 'André Marcon',
'Marie Cornillon' 'Marie Cornillon'
], ],
writer: ['Xavier Giannoli'], writer: ['Xavier Giannoli'],
rating: { rating: {
system: 'CSA', system: 'CSA',
value: '-10' value: '-10'
} }
} }
]) ])
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const result = await parser({ content }) const result = await parser({ content })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,92 +1,92 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'cgates.lt', site: 'cgates.lt',
days: 2, days: 2,
url: function ({ channel }) { url: function ({ channel }) {
return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/`
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
let programs = [] let programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
description: parseDescription($item), description: parseDescription($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
let html = await axios let html = await axios
.get('https://www.cgates.lt/televizija/tv-programa-savaitei/') .get('https://www.cgates.lt/televizija/tv-programa-savaitei/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
let $ = cheerio.load(html) let $ = cheerio.load(html)
const items = $('.vc_tta-panel.vc_active .kanalas_wrap').toArray() const items = $('.vc_tta-panel.vc_active .kanalas_wrap').toArray()
return items.map(item => { return items.map(item => {
const name = $(item).find('h6').text().trim() const name = $(item).find('h6').text().trim()
const link = $(item).find('a').attr('href') const link = $(item).find('a').attr('href')
const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null]
return { return {
lang: 'lt', lang: 'lt',
site_id, site_id,
name name
} }
}) })
} }
} }
function parseTitle($item) { function parseTitle($item) {
const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim()
return title || $item('td:nth-child(2)').text().trim() return title || $item('td:nth-child(2)').text().trim()
} }
function parseDescription($item) { function parseDescription($item) {
return $item('.vc_toggle_content > p').text().trim() return $item('.vc_toggle_content > p').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('.laikas') const time = $item('.laikas')
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius')
} }
function parseItems(content, date) { function parseItems(content, date) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
const section = $( const section = $(
'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div'
) )
.filter(function () { .filter(function () {
return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1
}) })
.first() .first()
return $('.tv_programa tr', section).toArray() return $('.tv_programa tr', section).toArray()
} }

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./cgates.lt.config.js') const { parser, url } = require('./cgates.lt.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'lrt-televizija-hd', site_id: 'lrt-televizija-hd',
xmltv_id: 'LRTTV.lt' xmltv_id: 'LRTTV.lt'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => { const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(35) expect(results.length).toBe(35)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-08-29T21:05:00.000Z', start: '2022-08-29T21:05:00.000Z',
stop: '2022-08-29T21:30:00.000Z', stop: '2022-08-29T21:30:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016', title: '31-oji nuovada (District 31), Drama, 2016',
description: description:
'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.' 'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.'
}) })
expect(results[34]).toMatchObject({ expect(results[34]).toMatchObject({
start: '2022-08-30T20:45:00.000Z', start: '2022-08-30T20:45:00.000Z',
stop: '2022-08-30T21:15:00.000Z', stop: '2022-08-30T21:15:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016!' title: '31-oji nuovada (District 31), Drama, 2016!'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '' content: ''
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,55 +1,55 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'chada.ma', site: 'chada.ma',
channels: 'chada.ma.channels.xml', channels: 'chada.ma.channels.xml',
days: 1, days: 1,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url() { url() {
return 'https://chada.ma/fr/chada-tv/grille-tv/' return 'https://chada.ma/fr/chada-tv/grille-tv/'
}, },
parser: function ({ content }) { parser: function ({ content }) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
const programs = [] const programs = []
$('#stopfix .posts-area h2').each((i, element) => { $('#stopfix .posts-area h2').each((i, element) => {
const timeRange = $(element).text().trim() const timeRange = $(element).text().trim()
const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim())) const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim()))
const titleElement = $(element).next('div').next('h3') const titleElement = $(element).next('div').next('h3')
const title = titleElement.text().trim() const title = titleElement.text().trim()
const description = titleElement.next('div').text().trim() || 'No description available' const description = titleElement.next('div').text().trim() || 'No description available'
programs.push({ programs.push({
title, title,
description, description,
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseProgramTime(timeStr) { function parseProgramTime(timeStr) {
const timeZone = 'Africa/Casablanca' const timeZone = 'Africa/Casablanca'
const currentDate = dayjs().format('YYYY-MM-DD') const currentDate = dayjs().format('YYYY-MM-DD')
return dayjs return dayjs
.tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone) .tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone)
.format('YYYY-MM-DDTHH:mm:ssZ') .format('YYYY-MM-DDTHH:mm:ssZ')
} }

View File

@@ -1,100 +1,100 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'clickthecity.com', site: 'clickthecity.com',
days: 2, days: 2,
url({ channel }) { url({ channel }) {
return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}`
}, },
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'content-type': 'application/x-www-form-urlencoded' 'content-type': 'application/x-www-form-urlencoded'
}, },
data({ date }) { data({ date }) {
const params = new URLSearchParams() const params = new URLSearchParams()
params.append( params.append(
'optDate', 'optDate',
DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd')
) )
params.append('optTime', '00:00:00') params.append('optTime', '00:00:00')
return params return params
} }
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
let stop = parseStop($item, date) let stop = parseStop($item, date)
if (!start || !stop) return if (!start || !stop) return
if (start > stop) { if (start > stop) {
stop = stop.plus({ days: 1 }) stop = stop.plus({ days: 1 })
} }
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const html = await axios const html = await axios
.get('https://www.clickthecity.com/tv/channels/') .get('https://www.clickthecity.com/tv/channels/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const items = $('#channels .col').toArray() const items = $('#channels .col').toArray()
return items.map(item => { return items.map(item => {
const name = $(item).find('.card-body').text().trim() const name = $(item).find('.card-body').text().trim()
const url = $(item).find('a').attr('href') const url = $(item).find('a').attr('href')
const [, site_id] = url.match(/netid=(\d+)/) || [null, null] const [, site_id] = url.match(/netid=(\d+)/) || [null, null]
return { return {
lang: 'en', lang: 'en',
site_id, site_id,
name name
} }
}) })
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('td > a').text().trim() return $item('td > a').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const url = $item('td.cPrg > a').attr('href') || '' const url = $item('td.cPrg > a').attr('href') || ''
let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null]
if (!time) return null if (!time) return null
time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}`
return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC()
} }
function parseStop($item, date) { function parseStop($item, date) {
const url = $item('td.cPrg > a').attr('href') || '' const url = $item('td.cPrg > a').attr('href') || ''
let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null]
if (!time) return null if (!time) return null
time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}`
return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC()
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('#tvlistings > tbody > tr') return $('#tvlistings > tbody > tr')
.filter(function () { .filter(function () {
return $(this).find('td.cPrg').length return $(this).find('td.cPrg').length
}) })
.toArray() .toArray()
} }

View File

@@ -1,67 +1,67 @@
const { parser, url, request } = require('./clickthecity.com.config.js') const { parser, url, request } = require('./clickthecity.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '5', site_id: '5',
xmltv_id: 'TV5.ph' xmltv_id: 'TV5.ph'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'content-type': 'application/x-www-form-urlencoded' 'content-type': 'application/x-www-form-urlencoded'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const result = request.data({ date }) const result = request.data({ date })
expect(result.get('optDate')).toBe('2023-06-12') expect(result.get('optDate')).toBe('2023-06-12')
expect(result.get('optTime')).toBe('00:00:00') expect(result.get('optTime')).toBe('00:00:00')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => { const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(20) expect(results.length).toBe(20)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-06-11T21:00:00.000Z', start: '2023-06-11T21:00:00.000Z',
stop: '2023-06-11T22:00:00.000Z', stop: '2023-06-11T22:00:00.000Z',
title: 'Word Of God' title: 'Word Of God'
}) })
expect(results[19]).toMatchObject({ expect(results[19]).toMatchObject({
start: '2023-06-12T15:30:00.000Z', start: '2023-06-12T15:30:00.000Z',
stop: '2023-06-12T16:00:00.000Z', stop: '2023-06-12T16:00:00.000Z',
title: 'La Suerte De Loli' title: 'La Suerte De Loli'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: content:
'<!DOCTYPE html><html class="html" lang="en-US" prefix="og: https://ogp.me/ns#"><head></head><body></body></html>' '<!DOCTYPE html><html class="html" lang="en-US" prefix="og: https://ogp.me/ns#"><head></head><body></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,137 +1,137 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my' const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my'
module.exports = { module.exports = {
site: 'content.astro.com.my', site: 'content.astro.com.my',
days: 2, days: 2,
url: function ({ channel }) { url: function ({ channel }) {
return `${API_ENDPOINT}/channel/${channel.site_id}.json` return `${API_ENDPOINT}/channel/${channel.site_id}.json`
}, },
async parser({ content, date }) { async parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
for (let item of items) { for (let item of items) {
const start = dayjs.utc(item.datetimeInUtc) const start = dayjs.utc(item.datetimeInUtc)
const duration = parseDuration(item.duration) const duration = parseDuration(item.duration)
const stop = start.add(duration, 's') const stop = start.add(duration, 's')
const details = await loadProgramDetails(item) const details = await loadProgramDetails(item)
programs.push({ programs.push({
title: details.title, title: details.title,
sub_title: item.subtitles, sub_title: item.subtitles,
description: details.longSynopsis || details.shortSynopsis, description: details.longSynopsis || details.shortSynopsis,
actors: parseList(details.cast), actors: parseList(details.cast),
directors: parseList(details.director), directors: parseList(details.director),
image: details.imageUrl, image: details.imageUrl,
rating: parseRating(details), rating: parseRating(details),
categories: parseCategories(details), categories: parseCategories(details),
episode: parseEpisode(item), episode: parseEpisode(item),
season: parseSeason(details), season: parseSeason(details),
start: start, start: start,
stop: stop stop: stop
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get('https://contenthub-api.eco.astro.com.my/channel/all.json') .get('https://contenthub-api.eco.astro.com.my/channel/all.json')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.response.map(item => { return data.response.map(item => {
return { return {
lang: 'ms', lang: 'ms',
site_id: item.id, site_id: item.id,
name: item.title name: item.title
} }
}) })
} }
} }
function parseEpisode(item) { function parseEpisode(item) {
const [, number] = item.title.match(/Ep(\d+)$/) || [null, null] const [, number] = item.title.match(/Ep(\d+)$/) || [null, null]
return number ? parseInt(number) : null return number ? parseInt(number) : null
} }
function parseSeason(details) { function parseSeason(details) {
const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null] const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null]
return season ? parseInt(season) : null return season ? parseInt(season) : null
} }
function parseList(list) { function parseList(list) {
return typeof list === 'string' ? list.split(',') : [] return typeof list === 'string' ? list.split(',') : []
} }
function parseRating(details) { function parseRating(details) {
return details.certification return details.certification
? { ? {
system: 'LPF', system: 'LPF',
value: details.certification value: details.certification
} }
: null : null
} }
function parseItems(content, date) { function parseItems(content, date) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
const schedules = data.response.schedule const schedules = data.response.schedule
return schedules[date.format('YYYY-MM-DD')] || [] return schedules[date.format('YYYY-MM-DD')] || []
} catch { } catch {
return [] return []
} }
} }
function parseDuration(duration) { function parseDuration(duration) {
const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/) const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/)
const hours = parseInt(match[1]) const hours = parseInt(match[1])
const minutes = parseInt(match[2]) const minutes = parseInt(match[2])
const seconds = parseInt(match[3]) const seconds = parseInt(match[3])
return hours * 3600 + minutes * 60 + seconds return hours * 3600 + minutes * 60 + seconds
} }
function parseCategories(details) { function parseCategories(details) {
const genres = { const genres = {
'filter/2': 'Action', 'filter/2': 'Action',
'filter/4': 'Anime', 'filter/4': 'Anime',
'filter/12': 'Cartoons', 'filter/12': 'Cartoons',
'filter/16': 'Comedy', 'filter/16': 'Comedy',
'filter/19': 'Crime', 'filter/19': 'Crime',
'filter/24': 'Drama', 'filter/24': 'Drama',
'filter/25': 'Educational', 'filter/25': 'Educational',
'filter/36': 'Horror', 'filter/36': 'Horror',
'filter/39': 'Live Action', 'filter/39': 'Live Action',
'filter/55': 'Pre-school', 'filter/55': 'Pre-school',
'filter/56': 'Reality', 'filter/56': 'Reality',
'filter/60': 'Romance', 'filter/60': 'Romance',
'filter/68': 'Talk Show', 'filter/68': 'Talk Show',
'filter/69': 'Thriller', 'filter/69': 'Thriller',
'filter/72': 'Variety', 'filter/72': 'Variety',
'filter/75': 'Series', 'filter/75': 'Series',
'filter/100': 'Others (Children)' 'filter/100': 'Others (Children)'
} }
return Array.isArray(details.subFilter) return Array.isArray(details.subFilter)
? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean) ? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean)
: [] : []
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}` const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}`
const data = await axios const data = await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.catch(error => console.log(error.message)) .catch(error => console.log(error.message))
if (!data) return {} if (!data) return {}
return data.response || {} return data.response || {}
} }

View File

@@ -1,71 +1,71 @@
const { parser, url } = require('./content.astro.com.my.config.js') const { parser, url } = require('./content.astro.com.my.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '425', site_id: '425',
xmltv_id: 'TVBClassic.hk' xmltv_id: 'TVBClassic.hk'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json') expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json')
}) })
it('can parse response', async () => { it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if ( if (
url === url ===
'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653' 'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json')))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
let results = await parser({ content, channel, date }) let results = await parser({ content, channel, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(31) expect(results.length).toBe(31)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-10-30T16:10:00.000Z', start: '2022-10-30T16:10:00.000Z',
stop: '2022-10-30T17:02:00.000Z', stop: '2022-10-30T17:02:00.000Z',
title: 'Triumph in the Skies S1 Ep06', title: 'Triumph in the Skies S1 Ep06',
description: description:
'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?', 'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?',
actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'], actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'],
directors: ['Joe Ma Tak Chung'], directors: ['Joe Ma Tak Chung'],
image: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg', image: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg',
rating: { rating: {
system: 'LPF', system: 'LPF',
value: 'U' value: 'U'
}, },
episode: 6, episode: 6,
season: 1, season: 1,
categories: ['Drama'] categories: ['Drama']
}) })
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
const results = await parser({ date, content }) const results = await parser({ date, content })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,85 +1,85 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(timezone) dayjs.extend(timezone)
module.exports = { module.exports = {
site: 'cosmotetv.gr', site: 'cosmotetv.gr',
days: 5, days: 5,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
}, },
method: 'GET', method: 'GET',
headers: { headers: {
referer: 'https://www.cosmotetv.gr/', referer: 'https://www.cosmotetv.gr/',
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
Accept: '*/*', Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9', 'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept-Encoding': 'gzip, deflate, br, zstd',
Origin: 'https://www.cosmotetv.gr', Origin: 'https://www.cosmotetv.gr',
'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"', 'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"',
'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site' 'Sec-Fetch-Site': 'cross-site'
} }
}, },
url: function ({ date, channel }) { url: function ({ date, channel }) {
const startOfDay = dayjs(date).startOf('day').utc().unix() const startOfDay = dayjs(date).startOf('day').utc().unix()
const endOfDay = dayjs(date).endOf('day').utc().unix() const endOfDay = dayjs(date).endOf('day').utc().unix()
return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false` return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`
}, },
parser: function ({ content }) { parser: function ({ content }) {
let programs = [] let programs = []
const data = JSON.parse(content) const data = JSON.parse(content)
data.channels.forEach(channel => { data.channels.forEach(channel => {
channel.items.forEach(item => { channel.items.forEach(item => {
const start = dayjs(item.startTime).utc().toISOString() const start = dayjs(item.startTime).utc().toISOString()
const stop = dayjs(item.endTime).utc().toISOString() const stop = dayjs(item.endTime).utc().toISOString()
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description || 'No description available', description: item.description || 'No description available',
category: item.qoe.genre, category: item.qoe.genre,
image: item.thumbnails.standard, image: item.thumbnails.standard,
start, start,
stop stop
}) })
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
try { try {
const response = await axios.get( const response = await axios.get(
'https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el', 'https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el',
{ {
headers: this.request.headers headers: this.request.headers
} }
) )
const data = response.data const data = response.data
if (data && data.channels) { if (data && data.channels) {
return data.channels.map(item => ({ return data.channels.map(item => ({
lang: 'el', lang: 'el',
site_id: item.callSign, site_id: item.callSign,
name: item.title name: item.title
//logo: item.logos.square //logo: item.logos.square
})) }))
} else { } else {
console.error('Unexpected response structure:', data) console.error('Unexpected response structure:', data)
return [] return []
} }
} catch (error) { } catch (error) {
console.error('Error fetching channel data:', error) console.error('Error fetching channel data:', error)
return [] return []
} }
} }
} }

View File

@@ -1,114 +1,114 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'cubmu.com', site: 'cubmu.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format( return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&channel_id=${channel.site_id}` )}&channel_id=${channel.site_id}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: parseTitle(item), title: parseTitle(item),
description: parseDescription(item, channel.lang), description: parseDescription(item, channel.lang),
episode: parseEpisode(item), episode: parseEpisode(item),
start: parseStart(item).toISOString(), start: parseStart(item).toISOString(),
stop: parseStop(item).toISOString() stop: parseStop(item).toISOString()
}) })
}) })
return programs return programs
}, },
async channels({ lang = 'id' }) { async channels({ lang = 'id' }) {
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const result = await axios const result = await axios
.get('https://cubmu.com/live-tv') .get('https://cubmu.com/live-tv')
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
const $ = cheerio.load(result) const $ = cheerio.load(result)
// retrieve service api data // retrieve service api data
const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {} const config = JSON.parse($('#__NEXT_DATA__').text()).runtimeConfig || {}
const options = { const options = {
headers: { headers: {
Origin: 'https://cubmu.com', Origin: 'https://cubmu.com',
Referer: 'https://cubmu.com/live-tv' Referer: 'https://cubmu.com/live-tv'
} }
} }
// login to service bus // login to service bus
await axios await axios
.post( .post(
`https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`, `https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`,
options options
) )
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
// list channels // list channels
const subscribedChannels = await axios const subscribedChannels = await axios
.post( .post(
`https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`, `https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`,
options options
) )
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
const channels = [] const channels = []
const included = [] const included = []
if (Array.isArray(subscribedChannels.channelPackageList)) { if (Array.isArray(subscribedChannels.channelPackageList)) {
subscribedChannels.channelPackageList.forEach(pkg => { subscribedChannels.channelPackageList.forEach(pkg => {
pkg.channelList.forEach(channel => { pkg.channelList.forEach(channel => {
if (included.indexOf(channel.id) < 0) { if (included.indexOf(channel.id) < 0) {
included.push(channel.id) included.push(channel.id)
channels.push({ channels.push({
lang, lang,
site_id: channel.id, site_id: channel.id,
name: channel.name name: channel.name
}) })
} }
}) })
}) })
} }
return channels return channels
} }
} }
function parseItems(content) { function parseItems(content) {
return content ? JSON.parse(content.trim()).result || [] : [] return content ? JSON.parse(content.trim()).result || [] : []
} }
function parseTitle(item) { function parseTitle(item) {
return item.scehedule_title return item.scehedule_title
} }
function parseDescription(item, lang = 'id') { function parseDescription(item, lang = 'id') {
return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis return lang === 'id' ? item.schedule_json.primarySynopsis : item.schedule_json.secondarySynopsis
} }
function parseEpisode(item) { function parseEpisode(item) {
return item.schedule_json.episodeName return item.schedule_json.episodeName
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.schedule_date, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') return dayjs.tz(item.schedule_date, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz( return dayjs.tz(
[item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '), [item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '),
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
'Asia/Jakarta' 'Asia/Jakarta'
) )
} }

View File

@@ -1,59 +1,59 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'cyta.com.cy', site: 'cyta.com.cy',
days: 7, days: 7,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url: function ({ date, channel }) { url: function ({ date, channel }) {
// Get the epoch timestamp // Get the epoch timestamp
const todayEpoch = date.startOf('day').utc().valueOf() const todayEpoch = date.startOf('day').utc().valueOf()
// Get the epoch timestamp for the next day // Get the epoch timestamp for the next day
const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf()
return `https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=${todayEpoch}&endTimeEpoch=${nextDayEpoch}&language=1&channelIds=${channel.site_id}` return `https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=${todayEpoch}&endTimeEpoch=${nextDayEpoch}&language=1&channelIds=${channel.site_id}`
}, },
parser: function ({ content }) { parser: function ({ content }) {
const data = JSON.parse(content) const data = JSON.parse(content)
const programs = [] const programs = []
data.channelEpgs.forEach(channel => { data.channelEpgs.forEach(channel => {
channel.epgPlayables.forEach(epg => { channel.epgPlayables.forEach(epg => {
const start = new Date(epg.startTime).toISOString() const start = new Date(epg.startTime).toISOString()
const stop = new Date(epg.endTime).toISOString() const stop = new Date(epg.endTime).toISOString()
programs.push({ programs.push({
title: epg.name, title: epg.name,
start, start,
stop stop
}) })
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get('https://epg.cyta.com.cy/api/mediacatalog/fetchChannels?language=1') .get('https://epg.cyta.com.cy/api/mediacatalog/fetchChannels?language=1')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
return { return {
lang: 'el', lang: 'el',
site_id: item.id, site_id: item.id,
name: item.name name: item.name
} }
}) })
} }
} }

View File

@@ -1,49 +1,49 @@
const { url, parser } = require('./cyta.com.cy.config.js') const { url, parser } = require('./cyta.com.cy.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day')
const channel = { const channel = {
site_id: '561066', site_id: '561066',
xmltv_id: 'RIK1.cy' xmltv_id: 'RIK1.cy'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
const generatedUrl = url({ date, channel }) const generatedUrl = url({ date, channel })
expect(generatedUrl).toBe( expect(generatedUrl).toBe(
'https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=1735862400000&endTimeEpoch=1735948800000&language=1&channelIds=561066' 'https://epg.cyta.com.cy/api/mediacatalog/fetchEpg?startTimeEpoch=1735862400000&endTimeEpoch=1735948800000&language=1&channelIds=561066'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = ` const content = `
{ {
"channelEpgs": [ "channelEpgs": [
{ {
"epgPlayables": [ "epgPlayables": [
{ "name": "Πρώτη Ενημέρωση", "startTime": 1735879500000, "endTime": 1735889400000 } { "name": "Πρώτη Ενημέρωση", "startTime": 1735879500000, "endTime": 1735889400000 }
] ]
} }
] ]
}` }`
const result = parser({ content }) const result = parser({ content })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: 'Πρώτη Ενημέρωση', title: 'Πρώτη Ενημέρωση',
start: '2025-01-03T04:45:00.000Z', start: '2025-01-03T04:45:00.000Z',
stop: '2025-01-03T07:30:00.000Z' stop: '2025-01-03T07:30:00.000Z'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '{"channelEpgs":[]}' content: '{"channelEpgs":[]}'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,73 +1,73 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const tz = 'Asia/Jakarta' const tz = 'Asia/Jakarta'
module.exports = { module.exports = {
site: 'dens.tv', site: 'dens.tv',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format( return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&id_channel=${channel.site_id}&app_type=10` )}&id_channel=${channel.site_id}&app_type=10`
}, },
parser({ content }) { parser({ content }) {
// parsing // parsing
const response = JSON.parse(content) const response = JSON.parse(content)
const programs = [] const programs = []
if (Array.isArray(response?.data)) { if (Array.isArray(response?.data)) {
response.data.forEach(item => { response.data.forEach(item => {
const title = item.title const title = item.title
const [, , , season, , , episode] = title.match( const [, , , season, , , episode] = title.match(
/( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/ /( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/
) || [null, null, null, null, null, null, null] ) || [null, null, null, null, null, null, null]
programs.push({ programs.push({
title, title,
description: item.description, description: item.description,
season: season ? parseInt(season) : season, season: season ? parseInt(season) : season,
episode: episode ? parseInt(episode) : episode, episode: episode ? parseInt(episode) : episode,
start: dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', tz), start: dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', tz),
stop: dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', tz) stop: dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', tz)
}) })
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const categories = { const categories = {
local: 1, local: 1,
premium: 2, premium: 2,
international: 3 international: 3
} }
const channels = [] const channels = []
for (const id_category of Object.values(categories)) { for (const id_category of Object.values(categories)) {
const data = await axios const data = await axios
.get('https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory', { .get('https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory', {
params: { id_category } params: { id_category }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
data.data.contents.forEach(item => { data.data.contents.forEach(item => {
channels.push({ channels.push({
lang: 'id', lang: 'id',
site_id: item.meta.id, site_id: item.meta.id,
name: item.meta.title name: item.meta.title
}) })
}) })
} }
return channels return channels
} }
} }

View File

@@ -1,50 +1,50 @@
const { url, parser } = require('./dens.tv.config.js') const { url, parser } = require('./dens.tv.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2024-11-24').startOf('d') const date = dayjs.utc('2024-11-24').startOf('d')
const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' } const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10' 'https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content }) let results = parser({ content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(2) expect(results.length).toBe(2)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2024-11-23T17:00:00.000Z', start: '2024-11-23T17:00:00.000Z',
stop: '2024-11-23T17:30:00.000Z', stop: '2024-11-23T17:30:00.000Z',
title: 'Migi & Dali Episode 2', title: 'Migi & Dali Episode 2',
episode: 2 episode: 2
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
start: '2024-11-23T19:30:00.000Z', start: '2024-11-23T19:30:00.000Z',
stop: '2024-11-23T20:00:00.000Z', stop: '2024-11-23T20:00:00.000Z',
title: 'Attack on Titan Season 3 Episode 7', title: 'Attack on Titan Season 3 Episode 7',
season: 3, season: 3,
episode: 7 episode: 7
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const results = parser({ content }) const results = parser({ content })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,50 +1,50 @@
const { parser, url } = require('./derana.lk.config.js') const { parser, url } = require('./derana.lk.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-05-18', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-05-18', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe('https://derana.lk/api/schedules/18-05-2025') expect(url({ date })).toBe('https://derana.lk/api/schedules/18-05-2025')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content }) let results = parser({ content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(20) expect(results.length).toBe(20)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'Dahami Derana', title: 'Dahami Derana',
image: 'https://derana.lk/storage/uploads/imgs/program/51/20240717062206.jpg', image: 'https://derana.lk/storage/uploads/imgs/program/51/20240717062206.jpg',
start: '2025-05-17T23:05:00.000Z', start: '2025-05-17T23:05:00.000Z',
stop: '2025-05-18T00:55:00.000Z' stop: '2025-05-18T00:55:00.000Z'
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
title: 'Derana Aruna', title: 'Derana Aruna',
image: 'https://derana.lk/storage/uploads/imgs/program/15/20240613075807.jpg', image: 'https://derana.lk/storage/uploads/imgs/program/15/20240613075807.jpg',
start: '2025-05-18T00:55:00.000Z', start: '2025-05-18T00:55:00.000Z',
stop: '2025-05-18T02:00:00.000Z' stop: '2025-05-18T02:00:00.000Z'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
content: { content: {
error: 'An error occurred' error: 'An error occurred'
} }
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,86 +1,86 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'digea.gr', site: 'digea.gr',
days: 2, days: 2,
url: 'https://www.digea.gr/el/api/epg/get-events', url: 'https://www.digea.gr/el/api/epg/get-events',
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
}, },
data({ date }) { data({ date }) {
const data = new URLSearchParams() const data = new URLSearchParams()
data.append('action', 'get_events') data.append('action', 'get_events')
data.append('date', date.format('YYYY-M-D')) data.append('date', date.format('YYYY-M-D'))
return data return data
} }
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
let items = parseItems(content, channel) let items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.long_synopsis, description: item.long_synopsis,
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.post( .post(
'https://www.digea.gr/el/api/epg/get-channels', 'https://www.digea.gr/el/api/epg/get-channels',
new URLSearchParams({ new URLSearchParams({
action: 'get_chanels', action: 'get_chanels',
lang: 'el' lang: 'el'
}), }),
{ {
headers: { headers: {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
} }
} }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data.map(channel => { return data.map(channel => {
return { return {
lang: 'el', lang: 'el',
site_id: channel.id, site_id: channel.id,
name: channel.name name: channel.name
} }
}) })
} }
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.actual_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens') return dayjs.tz(item.actual_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens') return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Athens')
} }
function parseItems(content, channel) { function parseItems(content, channel) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!Array.isArray(data)) return [] if (!Array.isArray(data)) return []
return data.filter(p => p.channel_id === channel.site_id) return data.filter(p => p.channel_id === channel.site_id)
} catch { } catch {
return [] return []
} }
} }

View File

@@ -1,69 +1,69 @@
const { parser, url, request } = require('./digea.gr.config.js') const { parser, url, request } = require('./digea.gr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-17', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-17', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '1100', site_id: '1100',
xmltv_id: 'AlphaTV.gr' xmltv_id: 'AlphaTV.gr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://www.digea.gr/el/api/epg/get-events') expect(url).toBe('https://www.digea.gr/el/api/epg/get-events')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const data = request.data({ date }) const data = request.data({ date })
expect(data.get('action')).toBe('get_events') expect(data.get('action')).toBe('get_events')
expect(data.get('date')).toBe('2025-1-17') expect(data.get('date')).toBe('2025-1-17')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content, channel }) let results = parser({ content, channel })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(19) expect(results.length).toBe(19)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-16T23:30:00.000Z', start: '2025-01-16T23:30:00.000Z',
stop: '2025-01-17T01:30:00.000Z', stop: '2025-01-17T01:30:00.000Z',
title: '[K12] Το Ξεκαθάρισμα (A Score To Settle)', title: '[K12] Το Ξεκαθάρισμα (A Score To Settle)',
description: description:
"Περιπέτεια αμερικανικής παραγωγής 2019 [Το πρόγραμμα περιέχει σκηνές σεξουαλικές, βίας, χρήσης ναρκωτικών κι άλλων εξαρτησιογόνων ουσιών και απρεπή εκφορά λόγου]. Ο Φρανκ απελευθερώνεται από τη φυλακή πολλά χρόνια μετά αφού κατηγορήθηκε για ένα έγκλημα που δεν διέπραξε. Τώρα, ελεύθερος, ξεκινά μια πορεία εκδίκησης εναντίον των ανθρώπων των οποίων οι πράξεις τον έστειλαν στη φυλακή. Ηθοποιοί: Νίκολας Κέιτζ, Μπέντζαμιν Μπρατ, Νόα Λε Γκρος, Καρολίνα Γουίντρα. Σενάριο: Σον Κου, Τζον Νιούμαν. Σκηνοθεσία: Σον Κου. Διάρκεια: 94'. " "Περιπέτεια αμερικανικής παραγωγής 2019 [Το πρόγραμμα περιέχει σκηνές σεξουαλικές, βίας, χρήσης ναρκωτικών κι άλλων εξαρτησιογόνων ουσιών και απρεπή εκφορά λόγου]. Ο Φρανκ απελευθερώνεται από τη φυλακή πολλά χρόνια μετά αφού κατηγορήθηκε για ένα έγκλημα που δεν διέπραξε. Τώρα, ελεύθερος, ξεκινά μια πορεία εκδίκησης εναντίον των ανθρώπων των οποίων οι πράξεις τον έστειλαν στη φυλακή. Ηθοποιοί: Νίκολας Κέιτζ, Μπέντζαμιν Μπρατ, Νόα Λε Γκρος, Καρολίνα Γουίντρα. Σενάριο: Σον Κου, Τζον Νιούμαν. Σκηνοθεσία: Σον Κου. Διάρκεια: 94'. "
}) })
expect(results[18]).toMatchObject({ expect(results[18]).toMatchObject({
start: '2025-01-17T21:30:00.000Z', start: '2025-01-17T21:30:00.000Z',
stop: '2025-01-17T23:30:00.000Z', stop: '2025-01-17T23:30:00.000Z',
title: '[K8] Βασικά Καλησπέρα Σας', title: '[K8] Βασικά Καλησπέρα Σας',
description: description:
"Κωμωδία ελληνικής παραγωγής 1982. Δύο πειρατικοί ραδιοσταθμοί, εκ των οποίων ο ένας βάζει λαϊκά άσματα και ο άλλος ροκ μουσική, ανταγωνίζονται για την πρωτιά στην ακροαματικότητα. Ο ανταγωνισμός γίνεται βαθμηδόν όλο και πιο σκληρός, αλλά ξάφνου τα πράγματα αλλάζουν ρότα καθώς ο μεγαλοδύναμος έρως παρεμβαίνει και κάνει το θαύμα του. Παίζουν: Στάθης Ψάλτης, Πάνος Μιχαλόπουλος, Σταμάτης Γαρδέλης, Έφη Πίκουλα, Γιώργος Ρήγας, Γιάννης Μποσταντζόγλου, Σοφία Αλιμπέρτη, Καίτη Φίνου. Σκηνοθεσία - Σενάριο: Γιάννης Δαλιανίδης. Διάρκεια: 89'." "Κωμωδία ελληνικής παραγωγής 1982. Δύο πειρατικοί ραδιοσταθμοί, εκ των οποίων ο ένας βάζει λαϊκά άσματα και ο άλλος ροκ μουσική, ανταγωνίζονται για την πρωτιά στην ακροαματικότητα. Ο ανταγωνισμός γίνεται βαθμηδόν όλο και πιο σκληρός, αλλά ξάφνου τα πράγματα αλλάζουν ρότα καθώς ο μεγαλοδύναμος έρως παρεμβαίνει και κάνει το θαύμα του. Παίζουν: Στάθης Ψάλτης, Πάνος Μιχαλόπουλος, Σταμάτης Γαρδέλης, Έφη Πίκουλα, Γιώργος Ρήγας, Γιάννης Μποσταντζόγλου, Σοφία Αλιμπέρτη, Καίτη Φίνου. Σκηνοθεσία - Σενάριο: Γιάννης Δαλιανίδης. Διάρκεια: 89'."
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
content: '[]' content: '[]'
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,86 +1,86 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const tz = 'Europe/Istanbul' const tz = 'Europe/Istanbul'
module.exports = { module.exports = {
site: 'digiturk.com.tr', site: 'digiturk.com.tr',
days: 2, days: 2,
url({ date }) { url({ date }) {
return `https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=${ return `https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=${
encodeURIComponent(date.format('MM/DD/YYYY')) encodeURIComponent(date.format('MM/DD/YYYY'))
}+00%3A00%3A00` }+00%3A00%3A00`
}, },
request: { request: {
cache: { cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
} }
}, },
parser({ content, channel, date }) { parser({ content, channel, date }) {
const programs = [] const programs = []
if (content) { if (content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
$('.channelDetail').toArray() $('.channelDetail').toArray()
.forEach(item => { .forEach(item => {
const $item = $(item) const $item = $(item)
const title = $item.find('.tvGuideResult-box-wholeDates-title') const title = $item.find('.tvGuideResult-box-wholeDates-title')
if (title.length) { if (title.length) {
const channelId = title.attr('onclick') const channelId = title.attr('onclick')
if (channelId) { if (channelId) {
const site_id = channelId.match(/\s(\d+)\)/)[1] const site_id = channelId.match(/\s(\d+)\)/)[1]
if (channel.site_id === site_id) { if (channel.site_id === site_id) {
const startTime = $item.find('.tvGuideResult-box-wholeDates-time-hour').text().trim() const startTime = $item.find('.tvGuideResult-box-wholeDates-time-hour').text().trim()
const duration = $item.find('.tvGuideResult-box-wholeDates-time-totalMinute') const duration = $item.find('.tvGuideResult-box-wholeDates-time-totalMinute')
.text().trim().match(/\d+/)[0] .text().trim().match(/\d+/)[0]
const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${startTime}`, 'YYYY-MM-DD HH:mm', tz) const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${startTime}`, 'YYYY-MM-DD HH:mm', tz)
const stop = start.add(parseInt(duration), 'm') const stop = start.add(parseInt(duration), 'm')
programs.push({ programs.push({
title: title.text().trim(), title: title.text().trim(),
start, start,
stop stop
}) })
} }
} }
} }
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const channels = {} const channels = {}
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(this.url({ date: dayjs() })) .get(this.url({ date: dayjs() }))
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
const $ = cheerio.load(data) const $ = cheerio.load(data)
$('.channelContent').toArray() $('.channelContent').toArray()
.forEach(el => { .forEach(el => {
const item = $(el) const item = $(el)
const channelId = item.find('.channelDetail .tvGuideResult-box-wholeDates-title') const channelId = item.find('.channelDetail .tvGuideResult-box-wholeDates-title')
.first() .first()
.attr('onclick') .attr('onclick')
if (channelId) { if (channelId) {
const site_id = channelId.match(/\s(\d+)\)/)[1] const site_id = channelId.match(/\s(\d+)\)/)[1]
if (channels[site_id] === undefined) { if (channels[site_id] === undefined) {
channels[site_id] = { channels[site_id] = {
lang: 'tr', lang: 'tr',
site_id, site_id,
name: item.find('#channelID').val() name: item.find('#channelID').val()
} }
} }
} }
}) })
return Object.values(channels) return Object.values(channels)
} }
} }

View File

@@ -1,48 +1,48 @@
const { parser, url } = require('./digiturk.com.tr.config.js') const { parser, url } = require('./digiturk.com.tr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '351', site_id: '351',
xmltv_id: 'Nickelodeon.tr' xmltv_id: 'Nickelodeon.tr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=01%2F12%2F2025+00%3A00%3A00' 'https://www.digiturk.com.tr/Ajax/GetTvGuideFromDigiturk?Day=01%2F12%2F2025+00%3A00%3A00'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html'))
const results = parser({ content, channel, date }).map(p => { const results = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(57) expect(results.length).toBe(57)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-11T21:00:00.000Z', start: '2025-01-11T21:00:00.000Z',
stop: '2025-01-11T21:25:00.000Z', stop: '2025-01-11T21:25:00.000Z',
title: 'Sünger Bob Kare Pantolon' title: 'Sünger Bob Kare Pantolon'
}) })
expect(results[56]).toMatchObject({ expect(results[56]).toMatchObject({
start: '2025-01-12T17:40:00.000Z', start: '2025-01-12T17:40:00.000Z',
stop: '2025-01-12T18:00:00.000Z', stop: '2025-01-12T18:00:00.000Z',
title: 'Casagrande Ailesi' title: 'Casagrande Ailesi'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ content: '', channel, date }) const result = parser({ content: '', channel, date })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,100 +1,100 @@
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'directv.com.ar', site: 'directv.com.ar',
days: 2, days: 2,
url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming', url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming',
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;', Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;',
Accept: '*/*', Accept: '*/*',
'Accept-Language': 'es-419,es;q=0.9', 'Accept-Language': 'es-419,es;q=0.9',
Connection: 'keep-alive', Connection: 'keep-alive',
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
Origin: 'https://www.directv.com.ar', Origin: 'https://www.directv.com.ar',
Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD', Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD',
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
'sec-ch-ua-mobile': '?0', 'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"' 'sec-ch-ua-platform': '"Windows"'
}, },
data({ channel, date }) { data({ channel, date }) {
const [channelNum, channelName] = channel.site_id.split('#') const [channelNum, channelName] = channel.site_id.split('#')
return { return {
filterParameters: { filterParameters: {
day: date.date(), day: date.date(),
time: 0, time: 0,
minute: 0, minute: 0,
month: date.month() + 1, month: date.month() + 1,
year: date.year(), year: date.year(),
offSetValue: 0, offSetValue: 0,
homeScreenFilter: '', homeScreenFilter: '',
filtersScreenFilters: [''], filtersScreenFilters: [''],
isHd: '', isHd: '',
isChannelDetails: 'Y', isChannelDetails: 'Y',
channelNum, channelNum,
channelName: channelName.replace('&amp;', '&') channelName: channelName.replace('&amp;', '&')
} }
} }
} }
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description, description: item.description,
rating: parseRating(item), rating: parseRating(item),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
}) })
return programs return programs
} }
} }
function parseRating(item) { function parseRating(item) {
return item.rating return item.rating
? { ? {
system: 'MPA', system: 'MPA',
value: item.rating value: item.rating
} }
: null : null
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires')
} }
function parseItems(content, channel) { function parseItems(content, channel) {
if (!content) return [] if (!content) return []
let [ChannelNumber, ChannelName] = channel.site_id.split('#') let [ChannelNumber, ChannelName] = channel.site_id.split('#')
ChannelName = ChannelName.replace('&amp;', '&') ChannelName = ChannelName.replace('&amp;', '&')
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.d)) return [] if (!data || !Array.isArray(data.d)) return []
const channelData = data.d.find( const channelData = data.d.find(
c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName
) )
return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : []
} }

View File

@@ -1,85 +1,85 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'directv.com.uy', site: 'directv.com.uy',
days: 2, days: 2,
url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming', url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming',
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;'
}, },
data({ channel, date }) { data({ channel, date }) {
const [channelNum, channelName] = channel.site_id.split('#') const [channelNum, channelName] = channel.site_id.split('#')
return { return {
filterParameters: { filterParameters: {
day: date.date(), day: date.date(),
time: 0, time: 0,
minute: 0, minute: 0,
month: date.month() + 1, month: date.month() + 1,
year: date.year(), year: date.year(),
offSetValue: 0, offSetValue: 0,
filtersScreenFilters: [''], filtersScreenFilters: [''],
isHd: '', isHd: '',
isChannelDetails: 'Y', isChannelDetails: 'Y',
channelNum, channelNum,
channelName: channelName.replace('&amp;', '&') channelName: channelName.replace('&amp;', '&')
} }
} }
} }
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description, description: item.description,
rating: parseRating(item), rating: parseRating(item),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
}) })
return programs return programs
} }
} }
function parseRating(item) { function parseRating(item) {
return item.rating return item.rating
? { ? {
system: 'MPA', system: 'MPA',
value: item.rating value: item.rating
} }
: null : null
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo')
} }
function parseItems(content, channel) { function parseItems(content, channel) {
if (!content) return [] if (!content) return []
let [ChannelNumber, ChannelName] = channel.site_id.split('#') let [ChannelNumber, ChannelName] = channel.site_id.split('#')
ChannelName = ChannelName.replace('&amp;', '&') ChannelName = ChannelName.replace('&amp;', '&')
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.d)) return [] if (!data || !Array.isArray(data.d)) return []
const channelData = data.d.find( const channelData = data.d.find(
c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName
) )
return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : []
} }

View File

@@ -1,76 +1,76 @@
const { parser, url, request } = require('./directv.com.uy.config.js') const { parser, url, request } = require('./directv.com.uy.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '184#VTV', site_id: '184#VTV',
xmltv_id: 'VTV.uy' xmltv_id: 'VTV.uy'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({ expect(request.data({ channel, date })).toMatchObject({
filterParameters: { filterParameters: {
day: 29, day: 29,
time: 0, time: 0,
minute: 0, minute: 0,
month: 8, month: 8,
year: 2022, year: 2022,
offSetValue: 0, offSetValue: 0,
filtersScreenFilters: [''], filtersScreenFilters: [''],
isHd: '', isHd: '',
isChannelDetails: 'Y', isChannelDetails: 'Y',
channelNum: '184', channelNum: '184',
channelName: 'VTV' channelName: 'VTV'
} }
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => { const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-08-29T03:00:00.000Z', start: '2022-08-29T03:00:00.000Z',
stop: '2022-08-29T05:00:00.000Z', stop: '2022-08-29T05:00:00.000Z',
title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio', title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio',
description: description:
'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).', 'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).',
rating: { rating: {
system: 'MPA', system: 'MPA',
value: 'NR' value: 'NR'
} }
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '', content: '',
channel channel
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,118 +1,118 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'directv.com', site: 'directv.com',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
}, },
headers: { headers: {
'Accept-Language': 'en-US,en;q=0.5', 'Accept-Language': 'en-US,en;q=0.5',
Connection: 'keep-alive' Connection: 'keep-alive'
} }
}, },
url({ date, channel }) { url({ date, channel }) {
const [channelId, childId] = channel.site_id.split('#') const [channelId, childId] = channel.site_id.split('#')
return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}`
}, },
async parser({ content, channel }) { async parser({ content, channel }) {
const programs = [] const programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
for (let item of items) { for (let item of items) {
if (item.programID === '-1') continue if (item.programID === '-1') continue
const detail = await loadProgramDetail(item.programID) const detail = await loadProgramDetail(item.programID)
const start = parseStart(item) const start = parseStart(item)
const stop = start.add(item.duration, 'm') const stop = start.add(item.duration, 'm')
programs.push({ programs.push({
title: item.title, title: item.title,
sub_title: item.episodeTitle, sub_title: item.episodeTitle,
description: parseDescription(detail), description: parseDescription(detail),
rating: parseRating(item), rating: parseRating(item),
date: parseYear(detail), date: parseYear(detail),
category: item.subcategoryList, category: item.subcategoryList,
season: item.seasonNumber, season: item.seasonNumber,
episode: item.episodeNumber, episode: item.episodeNumber,
image: parseImage(item), image: parseImage(item),
start, start,
stop stop
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const codes = [10001] const codes = [10001]
let channels = [] let channels = []
for (let code of codes) { for (let code of codes) {
const html = await axios const html = await axios
.get('https://www.directv.com/guide', { .get('https://www.directv.com/guide', {
headers: { headers: {
cookie: `dtve-prospect-zip=${code}` cookie: `dtve-prospect-zip=${code}`
} }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const script = $('#dtvClientData').html() const script = $('#dtvClientData').html()
const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null]
const data = JSON.parse(json) const data = JSON.parse(json)
data.guideData.channels.forEach(item => { data.guideData.channels.forEach(item => {
channels.push({ channels.push({
lang: 'en', lang: 'en',
site_id: item.chNum, site_id: item.chNum,
name: item.chName name: item.chName
}) })
}) })
} }
return channels return channels
} }
} }
function parseDescription(detail) { function parseDescription(detail) {
return detail ? detail.description : null return detail ? detail.description : null
} }
function parseYear(detail) { function parseYear(detail) {
return detail ? detail.releaseYear : null return detail ? detail.releaseYear : null
} }
function parseRating(item) { function parseRating(item) {
return item.rating return item.rating
? { ? {
system: 'MPA', system: 'MPA',
value: item.rating value: item.rating
} }
: null : null
} }
function parseImage(item) { function parseImage(item) {
return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null
} }
function loadProgramDetail(programID) { function loadProgramDetail(programID) {
return axios return axios
.get(`https://www.directv.com/json/program/flip/${programID}`) .get(`https://www.directv.com/json/program/flip/${programID}`)
.then(r => r.data) .then(r => r.data)
.then(d => d.programDetail) .then(d => d.programDetail)
.catch(console.err) .catch(console.err)
} }
function parseStart(item) { function parseStart(item) {
return dayjs.utc(item.airTime) return dayjs.utc(item.airTime)
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data) return [] if (!data) return []
if (!Array.isArray(data.schedule)) return [] if (!Array.isArray(data.schedule)) return []
const [, childId] = channel.site_id.split('#') const [, childId] = channel.site_id.split('#')
const channelData = data.schedule.find(i => i.chId == childId) const channelData = data.schedule.find(i => i.chId == childId)
return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : []
} }

View File

@@ -1,96 +1,96 @@
const { parser, url } = require('./directv.com.config.js') const { parser, url } = require('./directv.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '249#249', site_id: '249#249',
xmltv_id: 'ComedyCentralEast.us' xmltv_id: 'ComedyCentralEast.us'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249'
) )
}) })
it('can parse response', done => { it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { if (url === 'https://www.directv.com/json/program/flip/MV001173520000') {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
}) })
} else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content, channel }) parser({ content, channel })
.then(result => { .then(result => {
result = result.map(p => { result = result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2023-01-14T23:00:00.000Z', start: '2023-01-14T23:00:00.000Z',
stop: '2023-01-15T01:00:00.000Z', stop: '2023-01-15T01:00:00.000Z',
title: 'Men in Black II', title: 'Men in Black II',
description: description:
'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.', 'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.',
date: '2002', date: '2002',
image: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', image: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg',
category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'],
rating: { rating: {
system: 'MPA', system: 'MPA',
value: 'TV14' value: 'TV14'
} }
}, },
{ {
start: '2023-01-15T06:00:00.000Z', start: '2023-01-15T06:00:00.000Z',
stop: '2023-01-15T06:30:00.000Z', stop: '2023-01-15T06:30:00.000Z',
title: 'South Park', title: 'South Park',
sub_title: 'Goth Kids 3: Dawn of the Posers', sub_title: 'Goth Kids 3: Dawn of the Posers',
description: 'The goth kids are sent to a camp for troubled children.', description: 'The goth kids are sent to a camp for troubled children.',
image: image:
'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg', 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg',
category: ['Series', 'Animation', 'Comedy'], category: ['Series', 'Animation', 'Comedy'],
season: 17, season: 17,
episode: 4, episode: 4,
rating: { rating: {
system: 'MPA', system: 'MPA',
value: 'TVMA' value: 'TVMA'
} }
} }
]) ])
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))
parser({ content, channel }) parser({ content, channel })
.then(result => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(done) .catch(done)
}) })

View File

@@ -1,167 +1,167 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
let authToken let authToken
module.exports = { module.exports = {
site: 'dishtv.in', site: 'dishtv.in',
days: 2, days: 2,
url: 'https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs', url: 'https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs',
request: { request: {
method: 'POST', method: 'POST',
async headers() { async headers() {
await fetchToken() await fetchToken()
return { return {
Authorization: authToken Authorization: authToken
} }
}, },
data({ channel, date }) { data({ channel, date }) {
return { return {
allowPastEvents: true, allowPastEvents: true,
channelid: channel.site_id, channelid: channel.site_id,
date: date.format('DD/MM/YYYY') date: date.format('DD/MM/YYYY')
} }
} }
}, },
parser: ({ content }) => { parser: ({ content }) => {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: parseTitle(item), title: parseTitle(item),
description: parseDescription(item), description: parseDescription(item),
category: parseCategory(item), category: parseCategory(item),
actors: item.credits.actors, actors: item.credits.actors,
directors: item.credits.directors, directors: item.credits.directors,
producers: item.credits.producers, producers: item.credits.producers,
date: item.productionyear, date: item.productionyear,
icon: parseIcon(item), icon: parseIcon(item),
image: parseImage(item), image: parseImage(item),
episode: parseEpisode(item), episode: parseEpisode(item),
start: dayjs(item.start), start: dayjs(item.start),
stop: dayjs(item.stop) stop: dayjs(item.stop)
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
await fetchToken() await fetchToken()
const totalPages = await fetchPages() const totalPages = await fetchPages()
const queue = Array.from(Array(totalPages).keys()).map(i => { const queue = Array.from(Array(totalPages).keys()).map(i => {
const data = new FormData() const data = new FormData()
data.append('pageNum', i + 1) data.append('pageNum', i + 1)
return { return {
method: 'post', method: 'post',
url: 'https://www.dishtv.in/services/epg/channels', url: 'https://www.dishtv.in/services/epg/channels',
data, data,
headers: { headers: {
'authorization-token': authToken 'authorization-token': authToken
} }
} }
}) })
const channels = [] const channels = []
for (let item of queue) { for (let item of queue) {
const data = await axios(item) const data = await axios(item)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
data.programDetailsByChannel.forEach(channel => { data.programDetailsByChannel.forEach(channel => {
channels.push({ channels.push({
lang: 'en', lang: 'en',
site_id: channel.channelid, site_id: channel.channelid,
name: channel.channelname name: channel.channelname
}) })
}) })
} }
return channels return channels
} }
} }
function parseTitle(item) { function parseTitle(item) {
return Object.values(item.regional) return Object.values(item.regional)
.map(region => ({ .map(region => ({
lang: region.languagecode, lang: region.languagecode,
value: region.title value: region.title
})) }))
.filter(i => Boolean(i.value)) .filter(i => Boolean(i.value))
} }
function parseDescription(item) { function parseDescription(item) {
return Object.values(item.regional) return Object.values(item.regional)
.map(region => ({ .map(region => ({
lang: region.languagecode, lang: region.languagecode,
value: region.desc value: region.desc
})) }))
.filter(i => Boolean(i.value)) .filter(i => Boolean(i.value))
} }
function parseCategory(item) { function parseCategory(item) {
return Object.values(item.regional) return Object.values(item.regional)
.map(region => ({ .map(region => ({
lang: region.languagecode, lang: region.languagecode,
value: region.genre value: region.genre
})) }))
.filter(i => Boolean(i.value)) .filter(i => Boolean(i.value))
} }
function parseEpisode(item) { function parseEpisode(item) {
return item['episode-num'] ? parseInt(item['episode-num']) : null return item['episode-num'] ? parseInt(item['episode-num']) : null
} }
function parseIcon(item) { function parseIcon(item) {
return item.programmeurl || null return item.programmeurl || null
} }
function parseImage(item) { function parseImage(item) {
return item?.images?.landscape?.['1280x720'] ? item.images.landscape['1280x720'] : null return item?.images?.landscape?.['1280x720'] ? item.images.landscape['1280x720'] : null
} }
function parseItems(content) { function parseItems(content) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
return Array.isArray(data) ? data : [] return Array.isArray(data) ? data : []
} catch { } catch {
return [] return []
} }
} }
async function fetchToken() { async function fetchToken() {
if (authToken) return if (authToken) return
const data = await axios const data = await axios
.post('https://www.dishtv.in/services/epg/signin', null, { .post('https://www.dishtv.in/services/epg/signin', null, {
headers: { headers: {
'sec-fetch-dest': 'empty', 'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors', 'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin', 'sec-fetch-site': 'same-origin',
'x-requested-with': 'XMLHttpRequest', 'x-requested-with': 'XMLHttpRequest',
Referer: 'https://www.dishtv.in/channel-guide.html' Referer: 'https://www.dishtv.in/channel-guide.html'
} }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
authToken = data.token authToken = data.token
} }
async function fetchPages() { async function fetchPages() {
const formData = new FormData() const formData = new FormData()
formData.append('pageNum', 1) formData.append('pageNum', 1)
const data = await axios const data = await axios
.post('https://www.dishtv.in/services/epg/channels', formData, { .post('https://www.dishtv.in/services/epg/channels', formData, {
headers: { 'authorization-token': authToken } headers: { 'authorization-token': authToken }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data.totalPages ? parseInt(data.totalPages) : 0 return data.totalPages ? parseInt(data.totalPages) : 0
} }

View File

@@ -1,140 +1,140 @@
const { parser, url, request } = require('./dishtv.in.config.js') const { parser, url, request } = require('./dishtv.in.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
axios.post.mockImplementation((url, data, params) => { axios.post.mockImplementation((url, data, params) => {
if ( if (
url === 'https://www.dishtv.in/services/epg/signin' && url === 'https://www.dishtv.in/services/epg/signin' &&
data === null && data === null &&
JSON.stringify(params) === JSON.stringify(params) ===
JSON.stringify({ JSON.stringify({
headers: { headers: {
'sec-fetch-dest': 'empty', 'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors', 'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin', 'sec-fetch-site': 'same-origin',
'x-requested-with': 'XMLHttpRequest', 'x-requested-with': 'XMLHttpRequest',
Referer: 'https://www.dishtv.in/channel-guide.html' Referer: 'https://www.dishtv.in/channel-guide.html'
} }
}) })
) { ) {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/session.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(content) data: JSON.parse(content)
}) })
} else { } else {
return Promise.resolve({ return Promise.resolve({
data: '' data: ''
}) })
} }
}) })
const date = dayjs.utc('2025-01-26', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-26', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '142639', xmltv_id: 'AndpriveHD.in' } const channel = { site_id: '142639', xmltv_id: 'AndpriveHD.in' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs') expect(url).toBe('https://epg.mysmartstick.com/dishtv/api/v1/epg/entities/programs')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', async () => { it('can generate valid request headers', async () => {
expect(await request.headers()).toMatchObject({ expect(await request.headers()).toMatchObject({
Authorization: Authorization:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRpZCI6ImRpc2h0di13ZWJzaXRlIiwicGxhdGZvcm0iOiJkaXNodHYiLCJpYXQiOjE3Mzc2ODIxNjEsImV4cCI6MTczNzc2ODU2MX0.sPrYfodVTbf1kJ-wGICDlnH-Yt3J0-mB-M2YROU8v2Q' 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRpZCI6ImRpc2h0di13ZWJzaXRlIiwicGxhdGZvcm0iOiJkaXNodHYiLCJpYXQiOjE3Mzc2ODIxNjEsImV4cCI6MTczNzc2ODU2MX0.sPrYfodVTbf1kJ-wGICDlnH-Yt3J0-mB-M2YROU8v2Q'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({ expect(request.data({ channel, date })).toMatchObject({
allowPastEvents: true, allowPastEvents: true,
channelid: '142639', channelid: '142639',
date: '26/01/2025' date: '26/01/2025'
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content }) let results = parser({ content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(16) expect(results.length).toBe(16)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-26T00:30:00.000Z', start: '2025-01-26T00:30:00.000Z',
stop: '2025-01-26T02:05:00.000Z', stop: '2025-01-26T02:05:00.000Z',
title: [ title: [
{ lang: 'en', value: 'Train to Busan 2: Peninsula' }, { lang: 'en', value: 'Train to Busan 2: Peninsula' },
{ lang: 'hi', value: 'ट्रेन टू बुसान 2: पेनीनसुला' }, { lang: 'hi', value: 'ट्रेन टू बुसान 2: पेनीनसुला' },
{ lang: 'ta', value: 'ட்ரெயின் டு பூசன் ப்ரெசென்ட்ஸ்: பெனின்சுலா' }, { lang: 'ta', value: 'ட்ரெயின் டு பூசன் ப்ரெசென்ட்ஸ்: பெனின்சுலா' },
{ lang: 'te', value: 'ట్రేన్ టు బూసాన్ ప్రజెంట్స్: పెనిన్సులా' } { lang: 'te', value: 'ట్రేన్ టు బూసాన్ ప్రజెంట్స్: పెనిన్సులా' }
], ],
description: [ description: [
{ {
lang: 'en', lang: 'en',
value: value:
'Jung Seok, a former soldier, along with his teammates, sets out on a mission to battle hordes of post-apocalyptic zombies in the Korean peninsula wastelands.' 'Jung Seok, a former soldier, along with his teammates, sets out on a mission to battle hordes of post-apocalyptic zombies in the Korean peninsula wastelands.'
}, },
{ {
lang: 'hi', lang: 'hi',
value: value:
'एक भूतपूर्व सैनिक जंग सोक अपने साथियों के साथ कोरियाई प्रायद्वीप के बंजर इलाकों में सर्वनाश के बाद की जोंबी से लड़ने के मिशन पर निकलता है।' 'एक भूतपूर्व सैनिक जंग सोक अपने साथियों के साथ कोरियाई प्रायद्वीप के बंजर इलाकों में सर्वनाश के बाद की जोंबी से लड़ने के मिशन पर निकलता है।'
}, },
{ {
lang: 'ta', lang: 'ta',
value: value:
'கொரிய தீபகற்பத்தின் தரிசு நிலங்களில் அபோகாலிப்டிக் ஜாம்பிக்களின் கூட்டத்தை எதிர்த்து தன் குழுவுடன் போரிடும் ஜங் சியோக்.' 'கொரிய தீபகற்பத்தின் தரிசு நிலங்களில் அபோகாலிப்டிக் ஜாம்பிக்களின் கூட்டத்தை எதிர்த்து தன் குழுவுடன் போரிடும் ஜங் சியோக்.'
}, },
{ {
lang: 'te', lang: 'te',
value: value:
'మాజీ సైనికుడు జంగ్ సియోక్ తన సహచరులతో కలిసి కొరియా ద్వీపకల్పంలో పోస్ట్-అపోకలిప్టిక్ జాంబీలతో యుద్దానికి సిద్దమవుతాడు.' 'మాజీ సైనికుడు జంగ్ సియోక్ తన సహచరులతో కలిసి కొరియా ద్వీపకల్పంలో పోస్ట్-అపోకలిప్టిక్ జాంబీలతో యుద్దానికి సిద్దమవుతాడు.'
} }
], ],
category: [ category: [
{ lang: 'en', value: 'Film' }, { lang: 'en', value: 'Film' },
{ lang: 'hi', value: 'फ़िल्म' }, { lang: 'hi', value: 'फ़िल्म' },
{ lang: 'ta', value: '??????????' }, { lang: 'ta', value: '??????????' },
{ lang: 'te', value: 'సినిమా' }, { lang: 'te', value: 'సినిమా' },
{ lang: 'mr', value: 'चित्रपट' } { lang: 'mr', value: 'चित्रपट' }
], ],
actors: [ actors: [
'Gang Dong-won', 'Gang Dong-won',
'Lee Jung-hyun', 'Lee Jung-hyun',
'Lee Re', 'Lee Re',
'Kwon Hae-hyo', 'Kwon Hae-hyo',
'John D. Michaels', 'John D. Michaels',
'Kim Min-jae', 'Kim Min-jae',
'Kim Doyun', 'Kim Doyun',
'Lee Ye-won', 'Lee Ye-won',
'Daniel Joey Albright', 'Daniel Joey Albright',
'Pierce Conran', 'Pierce Conran',
'Geoffrey Giuliano', 'Geoffrey Giuliano',
'Milan-Devi LaBrey' 'Milan-Devi LaBrey'
], ],
producers: [], producers: [],
directors: ['Yeon Sang-ho'], directors: ['Yeon Sang-ho'],
icon: 'https://dtil.tmsimg.com/assets/p17850257_v_h9_al.jpg?lock=880x660', icon: 'https://dtil.tmsimg.com/assets/p17850257_v_h9_al.jpg?lock=880x660',
image: 'https://dtil.tmsimg.com/assets/p17850257_v_h8_am.jpg?lock=1280x720', image: 'https://dtil.tmsimg.com/assets/p17850257_v_h8_am.jpg?lock=1280x720',
date: '2020' date: '2020'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: '[]' }) const results = parser({ content: '[]' })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,99 +1,99 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'dna.fi', site: 'dna.fi',
days: 2, days: 2,
url({ date, channel }) { url({ date, channel }) {
const beginTimestamp = date.add(2, 'h').valueOf() const beginTimestamp = date.add(2, 'h').valueOf()
const endTimestamp = date.add(1, 'd').add(2, 'h').subtract(1, 's').valueOf() const endTimestamp = date.add(1, 'd').add(2, 'h').subtract(1, 's').valueOf()
return `https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:${channel.site_id}&q=profile:pr&q=start-interval:${beginTimestamp}/${endTimestamp}` return `https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:${channel.site_id}&q=profile:pr&q=start-interval:${beginTimestamp}/${endTimestamp}`
}, },
parser({ content, date }) { parser({ content, date }) {
let programs = [] let programs = []
let items = parseItems(content, date) let items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const data = item?._embedded?.['xrtv:meta']?.data const data = item?._embedded?.['xrtv:meta']?.data
programs.push({ programs.push({
title: data?.title, title: data?.title,
subtitle: data?.episode_title, subtitle: data?.episode_title,
description: data?.description, description: data?.description,
season: data?.season_number, season: data?.season_number,
episode: data?.episode_number, episode: data?.episode_number,
date: data?.year, date: data?.year,
categories: parseCategories(item), categories: parseCategories(item),
rating: parseRating(data), rating: parseRating(data),
images: parseImages(item), images: parseImages(item),
directors: parseCast(data, 'director'), directors: parseCast(data, 'director'),
actors: parseCast(data, 'actors'), actors: parseCast(data, 'actors'),
start: dayjs(data?.start), start: dayjs(data?.start),
stop: dayjs(data?.end) stop: dayjs(data?.end)
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get('https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=profile:ch&limit=1000') .get('https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=profile:ch&limit=1000')
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data._embedded['xrtv:media-item'].map(c => { return data._embedded['xrtv:media-item'].map(c => {
return { return {
lang: 'fi', lang: 'fi',
site_id: c.datalistTerm, site_id: c.datalistTerm,
name: c.name name: c.name
} }
}) })
} }
} }
function parseCast(data, role) { function parseCast(data, role) {
if (!data[role] || !data[role].value) return [] if (!data[role] || !data[role].value) return []
return data[role].value.split(', ').map(name => ({ return data[role].value.split(', ').map(name => ({
lang: data[role].lang, lang: data[role].lang,
value: name value: name
})) }))
} }
function parseCategories(item) { function parseCategories(item) {
const categories = item?._embedded?.['xrtv:media-category'] const categories = item?._embedded?.['xrtv:media-category']
return Array.isArray(categories) ? categories.map(category => category.name) : [] return Array.isArray(categories) ? categories.map(category => category.name) : []
} }
function parseRating(data) { function parseRating(data) {
if (!data.age_rating) return null if (!data.age_rating) return null
return { return {
system: 'VET', system: 'VET',
value: data.age_rating value: data.age_rating
} }
} }
function parseImages(item) { function parseImages(item) {
const images = item?._embedded?.['xrtv:image'] const images = item?._embedded?.['xrtv:image']
return Array.isArray(images) ? images.map(image => image.src) : [] return Array.isArray(images) ? images.map(image => image.src) : []
} }
function parseItems(content, date) { function parseItems(content, date) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
let items = data?._embedded?.['xrtv:media-item'] let items = data?._embedded?.['xrtv:media-item']
items = Array.isArray(items) ? items : [] items = Array.isArray(items) ? items : []
items = items.filter(item => { items = items.filter(item => {
const start = item?._embedded?.['xrtv:meta']?.data?.start const start = item?._embedded?.['xrtv:meta']?.data?.start
if (!start) return false if (!start) return false
return date.isSame(dayjs(start), 'day') return date.isSame(dayjs(start), 'day')
}) })
return items return items
} catch { } catch {
return [] return []
} }
} }

View File

@@ -1,138 +1,138 @@
const { parser, url } = require('./dna.fi.config.js') const { parser, url } = require('./dna.fi.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-15', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-15', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'ch-216356', site_id: 'ch-216356',
xmltv_id: 'MTV3.fi' xmltv_id: 'MTV3.fi'
} }
it('can generate valid url', async () => { it('can generate valid url', async () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:ch-216356&q=profile:pr&q=start-interval:1736906400000/1736992799000' 'https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:ch-216356&q=profile:pr&q=start-interval:1736906400000/1736992799000'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ date, content }) let results = parser({ date, content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(20) expect(results.length).toBe(20)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-15T02:30:00.000Z', start: '2025-01-15T02:30:00.000Z',
stop: '2025-01-15T03:22:00.000Z', stop: '2025-01-15T03:22:00.000Z',
title: { title: {
lang: 'fi', lang: 'fi',
value: 'Next Level Chef' value: 'Next Level Chef'
}, },
subtitle: { subtitle: {
lang: 'fi', lang: 'fi',
value: 'Brunssi' value: 'Brunssi'
}, },
season: 1, season: 1,
episode: 6, episode: 6,
rating: { rating: {
system: 'VET', system: 'VET',
value: 'S' value: 'S'
}, },
date: '2022', date: '2022',
images: [ images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/37f/3851073346622580374_aspect_ratio_16_9_1.jpg' 'https://mts-pro-cache-vip.dna.fi/meme/v2/37f/3851073346622580374_aspect_ratio_16_9_1.jpg'
], ],
description: { description: {
lang: 'fi', lang: 'fi',
value: value:
'Kausi 1, 6/11. Brunssi. Päivän haasteessa valmistetaan rentoa brunssiruokaa. Yksi kilpailija tekee valtaisan virheen myöhästyessään annosten luovutuksesta. Amerikkalainen tosi-tv-sarja.' 'Kausi 1, 6/11. Brunssi. Päivän haasteessa valmistetaan rentoa brunssiruokaa. Yksi kilpailija tekee valtaisan virheen myöhästyessään annosten luovutuksesta. Amerikkalainen tosi-tv-sarja.'
}, },
categories: ['Reality TV', 'Entertainment', 'TV Show', 'Next Level Chef', 'Series 1'] categories: ['Reality TV', 'Entertainment', 'TV Show', 'Next Level Chef', 'Series 1']
}) })
expect(results[5]).toMatchObject({ expect(results[5]).toMatchObject({
title: { title: {
lang: 'fi', lang: 'fi',
value: 'Kauniit ja rohkeat (S)' value: 'Kauniit ja rohkeat (S)'
}, },
subtitle: { subtitle: {
lang: 'fi', lang: 'fi',
value: 'Parantava syleily' value: 'Parantava syleily'
}, },
start: '2025-01-15T08:30:00.000Z', start: '2025-01-15T08:30:00.000Z',
stop: '2025-01-15T09:00:00.000Z', stop: '2025-01-15T09:00:00.000Z',
season: 37, season: 37,
episode: 9380, episode: 9380,
rating: { rating: {
system: 'VET', system: 'VET',
value: 'S' value: 'S'
}, },
date: '2023', date: '2023',
images: [ images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/79e/6509488401145439178_aspect_ratio_16_9_1.jpg' 'https://mts-pro-cache-vip.dna.fi/meme/v2/79e/6509488401145439178_aspect_ratio_16_9_1.jpg'
], ],
description: { description: {
lang: 'fi', lang: 'fi',
value: value:
'Steffy on vähällä yllättää Hopen ja Carterin kesken herkän hetken. Ridgen kannustamana Taylor suostuu kokeilemaan Shandran parannusmenetelmää, ja pitkään padotut tunteet saavat viimein vapautua.' 'Steffy on vähällä yllättää Hopen ja Carterin kesken herkän hetken. Ridgen kannustamana Taylor suostuu kokeilemaan Shandran parannusmenetelmää, ja pitkään padotut tunteet saavat viimein vapautua.'
}, },
categories: [ categories: [
'Soap', 'Soap',
'Drama', 'Drama',
'Romance', 'Romance',
'Series', 'Series',
'TV Show', 'TV Show',
'The Bold and the Beautiful', 'The Bold and the Beautiful',
'Series 37' 'Series 37'
], ],
actors: [{ lang: 'en', value: 'Katherine Kelly Lang' }] actors: [{ lang: 'en', value: 'Katherine Kelly Lang' }]
}) })
expect(results[19]).toMatchObject({ expect(results[19]).toMatchObject({
start: '2025-01-15T16:30:00.000Z', start: '2025-01-15T16:30:00.000Z',
stop: '2025-01-15T17:00:00.000Z', stop: '2025-01-15T17:00:00.000Z',
title: { title: {
lang: 'fi', lang: 'fi',
value: 'Emmerdale (S)' value: 'Emmerdale (S)'
}, },
subtitle: { subtitle: {
lang: 'fi', lang: 'fi',
value: 'Epäilyksen varjossa' value: 'Epäilyksen varjossa'
}, },
season: 54, season: 54,
episode: 9845, episode: 9845,
rating: { rating: {
system: 'VET', system: 'VET',
value: 'S' value: 'S'
}, },
date: '2023', date: '2023',
images: [ images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/5e8/5978592001161112833_aspect_ratio_16_9_1.jpg' 'https://mts-pro-cache-vip.dna.fi/meme/v2/5e8/5978592001161112833_aspect_ratio_16_9_1.jpg'
], ],
description: { description: {
lang: 'fi', lang: 'fi',
value: value:
'Caleb haistaa palaneen käryä Craigin kuolemaan liittyen. Mackenzien yllätysvierailu antaa vahvistuksen Chloen päätökselle. Lydia pohtii, pitäisikö hänen mennä Craigin hautajaisiin. Dawnin supistukset säikäyttävät Rhonan.' 'Caleb haistaa palaneen käryä Craigin kuolemaan liittyen. Mackenzien yllätysvierailu antaa vahvistuksen Chloen päätökselle. Lydia pohtii, pitäisikö hänen mennä Craigin hautajaisiin. Dawnin supistukset säikäyttävät Rhonan.'
}, },
categories: ['Soap', 'Drama', 'Romance', 'Series', 'TV Show', 'Emmerdale', 'Series 54'], categories: ['Soap', 'Drama', 'Romance', 'Series', 'TV Show', 'Emmerdale', 'Series 54'],
directors: [ directors: [
{ lang: 'en', value: 'Ian Bevitt' }, { lang: 'en', value: 'Ian Bevitt' },
{ lang: 'en', value: 'Munir Malik' } { lang: 'en', value: 'Munir Malik' }
] ]
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
date, date,
content: '' content: ''
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,130 +1,130 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const duration = require('dayjs/plugin/duration') const duration = require('dayjs/plugin/duration')
const doFetch = require('@ntlab/sfetch') const doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:dsmart.com.tr') const debug = require('debug')('site:dsmart.com.tr')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(duration) dayjs.extend(duration)
doFetch.setDebugger(debug) doFetch.setDebugger(debug)
const channelsWithSchedule = true const channelsWithSchedule = true
const pageLimit = 10 const pageLimit = 10
const caches = {} const caches = {}
module.exports = { module.exports = {
site: 'dsmart.com.tr', site: 'dsmart.com.tr',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
} }
}, },
url({ date, page = 1 }) { url({ date, page = 1 }) {
return `https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=${ return `https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=${
page page
}&limit=${ }&limit=${
pageLimit pageLimit
}&day=${ }&day=${
date.format('YYYY-MM-DD') date.format('YYYY-MM-DD')
}` }`
}, },
async parser({ content, channel, date, useCache = true }) { async parser({ content, channel, date, useCache = true }) {
const programs = [] const programs = []
if (content) { if (content) {
if (typeof content === 'string') { if (typeof content === 'string') {
content = JSON.parse(content) content = JSON.parse(content)
} }
if (useCache) { if (useCache) {
const cacheKey = date.format('YYYYMMDD') const cacheKey = date.format('YYYYMMDD')
// cache whole channels for the day // cache whole channels for the day
if (caches[cacheKey] === undefined) { if (caches[cacheKey] === undefined) {
if (content?.data?.total) { if (content?.data?.total) {
const queues = [] const queues = []
const pages = Math.ceil(content.data.total / pageLimit) const pages = Math.ceil(content.data.total / pageLimit)
for (let page = 2; page <= pages; page++) { for (let page = 2; page <= pages; page++) {
queues.push(module.exports.url({ date, page })) queues.push(module.exports.url({ date, page }))
} }
await doFetch(queues, (url, res) => { await doFetch(queues, (url, res) => {
if (Array.isArray(res?.data?.channels)) { if (Array.isArray(res?.data?.channels)) {
content.data.channels.push(...res.data.channels) content.data.channels.push(...res.data.channels)
} }
}) })
caches[cacheKey] = content caches[cacheKey] = content
} }
} else { } else {
content = caches[cacheKey] content = caches[cacheKey]
} }
} }
if (Array.isArray(content?.data?.channels)) { if (Array.isArray(content?.data?.channels)) {
content.data.channels content.data.channels
.filter(i => i._id === channel.site_id) .filter(i => i._id === channel.site_id)
.forEach(i => { .forEach(i => {
if (i.schedule.length) { if (i.schedule.length) {
let dayStart, ofs let dayStart, ofs
programs.push(...i.schedule programs.push(...i.schedule
.map(p => { .map(p => {
const baseDate = dayjs.utc(p.day) const baseDate = dayjs.utc(p.day)
const startDate = dayjs.utc(p.start_date) const startDate = dayjs.utc(p.start_date)
// calculate base offset if needed // calculate base offset if needed
if (!dayStart) { if (!dayStart) {
dayStart = startDate dayStart = startDate
ofs = dayjs.duration(dayjs.utc(`${p.day.substr(0, 11)}${p.start_date.substr(11)}`).diff(baseDate)) ofs = dayjs.duration(dayjs.utc(`${p.day.substr(0, 11)}${p.start_date.substr(11)}`).diff(baseDate))
.asSeconds() .asSeconds()
} }
const delta = dayjs.duration(startDate.diff(dayStart)).asSeconds() const delta = dayjs.duration(startDate.diff(dayStart)).asSeconds()
// ignore days in duration // ignore days in duration
const [h, m, s] = (p.duration.includes(',') ? p.duration.split(',')[1].trim() : p.duration) const [h, m, s] = (p.duration.includes(',') ? p.duration.split(',')[1].trim() : p.duration)
.split(':').map(Number) .split(':').map(Number)
const duration = (h * 3600) + (m * 60) + s const duration = (h * 3600) + (m * 60) + s
const start = baseDate.add(ofs + delta, 's') const start = baseDate.add(ofs + delta, 's')
const stop = start.add(duration, 's') const stop = start.add(duration, 's')
return { return {
title: p.program_name, title: p.program_name,
description: p.description, description: p.description,
category: p.genre && p.genre.includes('/') ? category: p.genre && p.genre.includes('/') ?
p.genre.split('/').map(g => `${g.substr(0, 1).toUpperCase()}${g.substr(1)}`) : null, p.genre.split('/').map(g => `${g.substr(0, 1).toUpperCase()}${g.substr(1)}`) : null,
start, start,
stop stop
} }
}) })
) )
} }
}) })
} }
} }
return programs return programs
}, },
async channels() { async channels() {
const channels = [] const channels = []
const f = page => this.url({ date: dayjs(), page }) const f = page => this.url({ date: dayjs(), page })
let pages, page = 1 let pages, page = 1
const queues = [f(page)] const queues = [f(page)]
await doFetch(queues, (url, res) => { await doFetch(queues, (url, res) => {
if (!pages && res.data.total) { if (!pages && res.data.total) {
pages = Math.ceil(res.data.total / pageLimit) pages = Math.ceil(res.data.total / pageLimit)
while (page < pages) { while (page < pages) {
queues.push(f(++page)) queues.push(f(++page))
} }
} }
if (Array.isArray(res?.data?.channels)) { if (Array.isArray(res?.data?.channels)) {
channels.push(...res.data.channels channels.push(...res.data.channels
.filter(i => (channelsWithSchedule && i.schedule.length) || !channelsWithSchedule) .filter(i => (channelsWithSchedule && i.schedule.length) || !channelsWithSchedule)
.map(i => { .map(i => {
return { return {
lang: 'tr', lang: 'tr',
name: i.channel_name, name: i.channel_name,
site_id: i._id site_id: i._id
} }
}) })
) )
} }
}) })
return channels return channels
} }
} }

View File

@@ -1,82 +1,82 @@
const { parser, url } = require('./dsmart.com.tr.config.js') const { parser, url } = require('./dsmart.com.tr.config.js')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '5fe07f5dcfef0b1593275822', site_id: '5fe07f5dcfef0b1593275822',
xmltv_id: 'Sinema1001.tr' xmltv_id: 'Sinema1001.tr'
} }
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
const result = {} const result = {}
const urls = { const urls = {
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13': 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13':
'content1.json', 'content1.json',
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=2&limit=10&day=2025-01-13': 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=2&limit=10&day=2025-01-13':
'content2.json', 'content2.json',
} }
if (urls[url] !== undefined) { if (urls[url] !== undefined) {
result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString() result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString()
if (!urls[url].startsWith('content1')) { if (!urls[url].startsWith('content1')) {
result.data = JSON.parse(result.data) result.data = JSON.parse(result.data)
} }
} }
return Promise.resolve(result) return Promise.resolve(result)
}) })
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe( expect(url({ date })).toBe(
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13' 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13'
) )
}) })
it('can parse response', async () => { it('can parse response', async () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')).toString() const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')).toString()
const results = (await parser({ content, channel, date })).map(p => { const results = (await parser({ content, channel, date })).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(11) expect(results.length).toBe(11)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-12T21:30:00.000Z', start: '2025-01-12T21:30:00.000Z',
stop: '2025-01-12T23:30:00.000Z', stop: '2025-01-12T23:30:00.000Z',
title: 'Taksi Şoförü', title: 'Taksi Şoförü',
description: description:
'Vietnam savaşının izlerinin etkisindeki bir asker ve New York sokakları. Travis Bickle, geceleri taksi şoförlüğü yaptığı New Yorkta bir yandan da gündelik yaşama ayak uydurmaya çalışır. Çürümeye yüz tutmuş bir topluma karşı tutulan bir ayna niteliğindeki film, yönetmen Martin Scorsesenin kariyerinin en önemli filmlerinden biri olarak kabul görür.', 'Vietnam savaşının izlerinin etkisindeki bir asker ve New York sokakları. Travis Bickle, geceleri taksi şoförlüğü yaptığı New Yorkta bir yandan da gündelik yaşama ayak uydurmaya çalışır. Çürümeye yüz tutmuş bir topluma karşı tutulan bir ayna niteliğindeki film, yönetmen Martin Scorsesenin kariyerinin en önemli filmlerinden biri olarak kabul görür.',
category: ['Sinema', 'Genel'] category: ['Sinema', 'Genel']
}) })
expect(results[10]).toMatchObject({ expect(results[10]).toMatchObject({
start: '2025-01-13T19:00:00.000Z', start: '2025-01-13T19:00:00.000Z',
stop: '2025-01-13T21:00:00.000Z', stop: '2025-01-13T21:00:00.000Z',
title: 'Senin Adın', title: 'Senin Adın',
description: description:
'Dağların sardığı bir bölgede yaşayan Mitsuha, hayatından çok da memnun olmayan liseli bir kızdır. Babası vali olarak çalışmakta ve seçim kampanyaları ile uğraşmaktadır. Evde kendisi, kardeşi ve büyükannesi dışında kimse yoktur. Kırsal kesimdeki yaşamı onu bunaltmaktadır ve esas isteği Tokyo\'nun muhteşem şehir hayatının bir parçası olmaktır. Diğer tarafta ise Taki vardır.', 'Dağların sardığı bir bölgede yaşayan Mitsuha, hayatından çok da memnun olmayan liseli bir kızdır. Babası vali olarak çalışmakta ve seçim kampanyaları ile uğraşmaktadır. Evde kendisi, kardeşi ve büyükannesi dışında kimse yoktur. Kırsal kesimdeki yaşamı onu bunaltmaktadır ve esas isteği Tokyo\'nun muhteşem şehir hayatının bir parçası olmaktır. Diğer tarafta ise Taki vardır.',
category: ['Sinema', 'Genel'] category: ['Sinema', 'Genel']
}) })
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
const results = await parser({ const results = await parser({
channel, channel,
date, date,
content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')).toString(), content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')).toString(),
useCache: false useCache: false
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,111 +1,111 @@
const { parser, url } = require('./dstv.com.config.js') const { parser, url } = require('./dstv.com.config.js')
const axios = require('axios') const axios = require('axios')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide'
const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d')
const channelZA = { const channelZA = {
site_id: 'zaf#201', site_id: 'zaf#201',
xmltv_id: 'SuperSportGrandstand.za' xmltv_id: 'SuperSportGrandstand.za'
} }
const channelNG = { const channelNG = {
site_id: 'nga#201', site_id: 'nga#201',
xmltv_id: 'SuperSportGrandstand.za' xmltv_id: 'SuperSportGrandstand.za'
} }
it('can generate valid url for zaf', () => { it('can generate valid url for zaf', () => {
expect(url({ channel: channelZA, date })).toBe( expect(url({ channel: channelZA, date })).toBe(
`${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf` `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf`
) )
}) })
it('can generate valid url for nga', () => { it('can generate valid url for nga', () => {
expect(url({ channel: channelNG, date })).toBe( expect(url({ channel: channelNG, date })).toBe(
`${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga` `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga`
) )
}) })
it('can parse response for ZA', async () => { it('can parse response for ZA', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) { if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json')))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
let results = await parser({ content, channel: channelZA }) let results = await parser({ content, channel: channelZA })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
start: '2022-11-21T23:00:00.000Z', start: '2022-11-21T23:00:00.000Z',
stop: '2022-11-22T00:00:00.000Z', stop: '2022-11-22T00:00:00.000Z',
title: 'UFC FN HL: Nzechukwu v Cutelaba', title: 'UFC FN HL: Nzechukwu v Cutelaba',
description: description:
"'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.",
image: image:
'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png',
category: ['All Sport', 'Mixed Martial Arts'] category: ['All Sport', 'Mixed Martial Arts']
}) })
}) })
it('can parse response for NG', async () => { it('can parse response for NG', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) { if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json')))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
let results = await parser({ content, channel: channelNG }) let results = await parser({ content, channel: channelNG })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-11-21T23:00:00.000Z', start: '2022-11-21T23:00:00.000Z',
stop: '2022-11-22T00:00:00.000Z', stop: '2022-11-22T00:00:00.000Z',
title: 'UFC FN HL: Nzechukwu v Cutelaba', title: 'UFC FN HL: Nzechukwu v Cutelaba',
description: description:
"'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.",
image: image:
'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png',
category: ['All Sport', 'Mixed Martial Arts'] category: ['All Sport', 'Mixed Martial Arts']
}) })
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
parser({ parser({
content: '{"Total":0,"Channels":[]}', content: '{"Total":0,"Channels":[]}',
channel: channelZA channel: channelZA
}) })
.then(result => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(done) .catch(done)
}) })

View File

@@ -1,90 +1,90 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'dtv8.net', site: 'dtv8.net',
days: 2, days: 2,
url({ date }) { url({ date }) {
const day = date.format('dddd') const day = date.format('dddd')
return `https://dtv8.net/tv-listings/${day.toLowerCase()}/` return `https://dtv8.net/tv-listings/${day.toLowerCase()}/`
}, },
parser({ content, date }) { parser({ content, date }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
let prev = programs[programs.length - 1] let prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start < prev.start) { if (start < prev.start) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
description: parseDescription($item), description: parseDescription($item),
image: parseImage($item), image: parseImage($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
channels() { channels() {
return [] return []
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item( return $item(
'td:nth-child(2) > strong:nth-child(1),td:nth-child(2) > span > strong,td:nth-child(2) > span > b' 'td:nth-child(2) > strong:nth-child(1),td:nth-child(2) > span > strong,td:nth-child(2) > span > b'
).text() ).text()
} }
function parseDescription($item) { function parseDescription($item) {
return ( return (
$item( $item(
'td:nth-child(2) > strong:nth-child(3) > span,td:nth-child(2) > p:nth-child(3) > strong > span' 'td:nth-child(2) > strong:nth-child(3) > span,td:nth-child(2) > p:nth-child(3) > strong > span'
).text() || null ).text() || null
) )
} }
function parseImage($item) { function parseImage($item) {
return $item('td:nth-child(1) > img.size-full').attr('src') || null return $item('td:nth-child(1) > img.size-full').attr('src') || null
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('td:nth-child(1)').text() const time = $item('td:nth-child(1)').text()
return dayjs.tz( return dayjs.tz(
`${date.format('YYYY-MM-DD')} ${time}`, `${date.format('YYYY-MM-DD')} ${time}`,
'YYYY-MM-DD HH:mm [hrs.]', 'YYYY-MM-DD HH:mm [hrs.]',
'America/Guyana' 'America/Guyana'
) )
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('table tr') return $('table tr')
.filter((i, el) => { .filter((i, el) => {
const firstColumn = $(el).find('td').text() const firstColumn = $(el).find('td').text()
return Boolean(firstColumn) && !firstColumn.includes('Time') return Boolean(firstColumn) && !firstColumn.includes('Time')
}) })
.toArray() .toArray()
} }

View File

@@ -1,79 +1,79 @@
const { parser, url } = require('./dtv8.net.config.js') const { parser, url } = require('./dtv8.net.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-02-21', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-02-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe('https://dtv8.net/tv-listings/friday/') expect(url({ date })).toBe('https://dtv8.net/tv-listings/friday/')
}) })
it('can parse response for friday', () => { it('can parse response for friday', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_fri.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_fri.html'))
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(18) expect(results.length).toBe(18)
expect(results[9]).toMatchObject({ expect(results[9]).toMatchObject({
title: 'Smallville', title: 'Smallville',
image: 'http://dtv8.net/wp-content/uploads/71P0aShCBXL._SL1300_.jpg', image: 'http://dtv8.net/wp-content/uploads/71P0aShCBXL._SL1300_.jpg',
description: description:
'A young Clark Kent struggles to find his place in the world as he learns to harness his alien powers for good and deals with the typical troubles of teenage life in Smallville, Kansas.', 'A young Clark Kent struggles to find his place in the world as he learns to harness his alien powers for good and deals with the typical troubles of teenage life in Smallville, Kansas.',
start: '2025-02-21T21:00:00.000Z', start: '2025-02-21T21:00:00.000Z',
stop: '2025-02-21T22:00:00.000Z' stop: '2025-02-21T22:00:00.000Z'
}) })
expect(results[15]).toMatchObject({ expect(results[15]).toMatchObject({
title: 'Law & Order', title: 'Law & Order',
image: null, image: null,
description: description:
'In God We Trust: A young lawyer with a secret past is found dead; Price and Baxter debate the pros and cons of prison as a punishment versus alternative justice options.', 'In God We Trust: A young lawyer with a secret past is found dead; Price and Baxter debate the pros and cons of prison as a punishment versus alternative justice options.',
start: '2025-02-22T01:45:00.000Z', start: '2025-02-22T01:45:00.000Z',
stop: '2025-02-22T02:30:00.000Z' stop: '2025-02-22T02:30:00.000Z'
}) })
}) })
it('can parse response for saturday', () => { it('can parse response for saturday', () => {
const date = dayjs.utc('2025-02-22', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-02-22', 'YYYY-MM-DD').startOf('d')
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_sat.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_sat.html'))
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(11) expect(results.length).toBe(11)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'Sign On', title: 'Sign On',
image: null, image: null,
description: null, description: null,
start: '2025-02-22T13:55:00.000Z', start: '2025-02-22T13:55:00.000Z',
stop: '2025-02-22T14:00:00.000Z' stop: '2025-02-22T14:00:00.000Z'
}) })
expect(results[10]).toMatchObject({ expect(results[10]).toMatchObject({
title: 'Sign Off', title: 'Sign Off',
image: null, image: null,
description: null, description: null,
start: '2025-02-23T04:00:00.000Z', start: '2025-02-23T04:00:00.000Z',
stop: '2025-02-23T04:30:00.000Z' stop: '2025-02-23T04:30:00.000Z'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: '' }) const results = parser({ content: '' })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,149 +1,149 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
require('dayjs/locale/ar') require('dayjs/locale/ar')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(utc)
const headers = { const headers = {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0' } 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0' }
module.exports = { module.exports = {
site: 'elcinema.com', site: 'elcinema.com',
days: 2, days: 2,
request: { headers }, request: { headers },
url({ channel }) { url({ channel }) {
const lang = channel.lang === 'en' ? 'en/' : '/' const lang = channel.lang === 'en' ? 'en/' : '/'
return `https://elcinema.com/${lang}tvguide/${channel.site_id}/` return `https://elcinema.com/${lang}tvguide/${channel.site_id}/`
}, },
parser({ content, channel, date }) { parser({ content, channel, date }) {
const programs = [] const programs = []
const items = parseItems(content, channel, date) const items = parseItems(content, channel, date)
items.forEach(item => { items.forEach(item => {
const start = parseStart(item, date) const start = parseStart(item, date)
const duration = parseDuration(item) const duration = parseDuration(item)
const stop = start.add(duration, 'm') const stop = start.add(duration, 'm')
programs.push({ programs.push({
title: parseTitle(item), title: parseTitle(item),
description: parseDescription(item), description: parseDescription(item),
category: parseCategory(item), category: parseCategory(item),
image: parseImage(item), image: parseImage(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels({ lang }) { async channels({ lang }) {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://elcinema.com/${lang}/tvguide/`, { .get(`https://elcinema.com/${lang}/tvguide/`, {
headers: headers headers: headers
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(data) const $ = cheerio.load(data)
return $('.tv-line') return $('.tv-line')
.map((i, el) => { .map((i, el) => {
const link = $(el).find('.channel > div > div.hide-for-small-only > a') const link = $(el).find('.channel > div > div.hide-for-small-only > a')
const name = $(link).text() const name = $(link).text()
const href = $(link).attr('href') const href = $(link).attr('href')
const [, site_id] = href.match(/\/(\d+)\/$/) const [, site_id] = href.match(/\/(\d+)\/$/)
return { return {
lang, lang,
site_id, site_id,
name name
} }
}) })
.get() .get()
} }
} }
function parseImage(item) { function parseImage(item) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
const imgSrc = const imgSrc =
$('.row > div.columns.small-3.large-1 > a > img').data('src') || $('.row > div.columns.small-3.large-1 > a > img').data('src') ||
$('.row > div.columns.small-5.large-1 > img').data('src') $('.row > div.columns.small-5.large-1 > img').data('src')
return imgSrc || null return imgSrc || null
} }
function parseCategory(item) { function parseCategory(item) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text() const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text()
return category.replace(/\(\d+\)/, '').trim() || null return category.replace(/\(\d+\)/, '').trim() || null
} }
function parseDuration(item) { function parseDuration(item) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
const duration = const duration =
$('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() || $('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() ||
$('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text() $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text()
return duration.replace(/\D/g, '') || '' return duration.replace(/\D/g, '') || ''
} }
function parseStart(item, initDate) { function parseStart(item, initDate) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
let time = let time =
$('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() || $('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() ||
$('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() || $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() ||
'' ''
time = time time = time
.replace(/\[.*\]/, '') .replace(/\[.*\]/, '')
.replace('مساءً', 'PM') .replace('مساءً', 'PM')
.replace('صباحًا', 'AM') .replace('صباحًا', 'AM')
.trim() .trim()
time = `${initDate.format('YYYY-MM-DD')} ${time}` time = `${initDate.format('YYYY-MM-DD')} ${time}`
return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess()) return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess())
} }
function parseTitle(item) { function parseTitle(item) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
return ( return (
$('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() || $('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() ||
$('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() || $('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() ||
null null
) )
} }
function parseDescription(item) { function parseDescription(item) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || '' const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || ''
return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '') return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '')
} }
function parseItems(content, channel, date) { function parseItems(content, channel, date) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
const dateString = date.locale(channel.lang).format('dddd D') const dateString = date.locale(channel.lang).format('dddd D')
const list = $('.dates') const list = $('.dates')
.filter((i, el) => { .filter((i, el) => {
let parsedDateString = $(el).text().trim() let parsedDateString = $(el).text().trim()
parsedDateString = parsedDateString.replace(/\s\s+/g, ' ') parsedDateString = parsedDateString.replace(/\s\s+/g, ' ')
return parsedDateString.includes(dateString) return parsedDateString.includes(dateString)
}) })
.first() .first()
.parent() .parent()
.next() .next()
return $('.padded-half', list).toArray() return $('.padded-half', list).toArray()
} }

View File

@@ -1,69 +1,69 @@
const { parser, url } = require('./elcinema.com.config.js') const { parser, url } = require('./elcinema.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d')
const channelAR = { const channelAR = {
lang: 'ar', lang: 'ar',
site_id: '1254', site_id: '1254',
xmltv_id: 'OSNSeries.ae' xmltv_id: 'OSNSeries.ae'
} }
const channelEN = { const channelEN = {
lang: 'en', lang: 'en',
site_id: '1254', site_id: '1254',
xmltv_id: 'OSNSeries.ae' xmltv_id: 'OSNSeries.ae'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/') expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/')
}) })
it('can parse response (en)', () => { it('can parse response (en)', () => {
const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html')) const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html'))
const results = parser({ date, channel: channelEN, content: contentEN }).map(p => { const results = parser({ date, channel: channelEN, content: contentEN }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-08-27T14:25:00.000Z', start: '2022-08-27T14:25:00.000Z',
stop: '2022-08-27T15:15:00.000Z', stop: '2022-08-27T15:15:00.000Z',
title: 'Station 19 S5', title: 'Station 19 S5',
image: image:
'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg',
category: 'Series' category: 'Series'
}) })
}) })
it('can parse response (ar)', () => { it('can parse response (ar)', () => {
const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html')) const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html'))
const results = parser({ date, channel: channelAR, content: contentAR }).map(p => { const results = parser({ date, channel: channelAR, content: contentAR }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-08-27T14:25:00.000Z', start: '2022-08-27T14:25:00.000Z',
stop: '2022-08-27T15:15:00.000Z', stop: '2022-08-27T15:15:00.000Z',
title: 'Station 19 S5', title: 'Station 19 S5',
image: image:
'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg',
category: 'مسلسل' category: 'مسلسل'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel: channelEN, channel: channelEN,
content: '<!DOCTYPE html><html lang="ar" dir="rtl"><head></head><body></body></html>' content: '<!DOCTYPE html><html lang="ar" dir="rtl"><head></head><body></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,68 +1,68 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'ena.skylifetv.co.kr', site: 'ena.skylifetv.co.kr',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U` return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const start = parseStart($item, date) const start = parseStart($item, date)
const duration = parseDuration($item) const duration = parseDuration($item)
const stop = start.add(duration, 'm') const stop = start.add(duration, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
rating: parseRating($item), rating: parseRating($item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.col2 > .tit').text().trim() return $item('.col2 > .tit').text().trim()
} }
function parseRating($item) { function parseRating($item) {
const rating = $item('.col4').text().trim() const rating = $item('.col4').text().trim()
return rating return rating
? { ? {
system: 'KMRB', system: 'KMRB',
value: rating value: rating
} }
: null : null
} }
function parseDuration($item) { function parseDuration($item) {
const duration = $item('.col5').text().trim() const duration = $item('.col5').text().trim()
return duration ? parseInt(duration) : 30 return duration ? parseInt(duration) : 30
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('.col1').text().trim() const time = $item('.col1').text().trim()
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul')
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('.tbl_schedule > tbody > tr').toArray() return $('.tbl_schedule > tbody > tr').toArray()
} }

View File

@@ -1,57 +1,57 @@
const { parser, url } = require('./ena.skylifetv.co.kr.config.js') const { parser, url } = require('./ena.skylifetv.co.kr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-01-27', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-01-27', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'ENA', site_id: 'ENA',
xmltv_id: 'ENA.kr' xmltv_id: 'ENA.kr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U') expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-01-26T16:05:00.000Z', start: '2023-01-26T16:05:00.000Z',
stop: '2023-01-26T17:20:00.000Z', stop: '2023-01-26T17:20:00.000Z',
title: '법쩐 6화', title: '법쩐 6화',
rating: { rating: {
system: 'KMRB', system: 'KMRB',
value: '15' value: '15'
} }
}) })
expect(results[17]).toMatchObject({ expect(results[17]).toMatchObject({
start: '2023-01-27T14:10:00.000Z', start: '2023-01-27T14:10:00.000Z',
stop: '2023-01-27T15:25:00.000Z', stop: '2023-01-27T15:25:00.000Z',
title: '남이 될 수 있을까 4화', title: '남이 될 수 있을까 4화',
rating: { rating: {
system: 'KMRB', system: 'KMRB',
value: '15' value: '15'
} }
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,33 +1,33 @@
const parser = require('epg-parser') const parser = require('epg-parser')
module.exports = { module.exports = {
site: 'energeek.cl', site: 'energeek.cl',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml', url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml',
parser: function ({ content, channel, date }) { parser: function ({ content, channel, date }) {
let programs = [] let programs = []
const items = parseItems(content, channel, date) const items = parseItems(content, channel, date)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title?.[0]?.value, title: item.title?.[0]?.value,
description: item.desc?.[0]?.value, description: item.desc?.[0]?.value,
icon: item.icon?.[0]?.src, icon: item.icon?.[0]?.src,
start: item.start, start: item.start,
stop: item.stop stop: item.stop
}) })
}) })
return programs return programs
} }
} }
function parseItems(content, channel, date) { function parseItems(content, channel, date) {
const { programs } = parser.parse(content) const { programs } = parser.parse(content)
return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day'))
} }

View File

@@ -1,37 +1,37 @@
const { parser, url } = require('./energeek.cl.config.js') const { parser, url } = require('./energeek.cl.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'EnerGeek Retro', site_id: 'EnerGeek Retro',
xmltv_id: 'EnerGeekRetro.cl' xmltv_id: 'EnerGeekRetro.cl'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml') expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
let results = parser({ content, channel, date }) let results = parser({ content, channel, date })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-11-29T03:00:00.000Z', start: '2022-11-29T03:00:00.000Z',
stop: '2022-11-29T03:30:00.000Z', stop: '2022-11-29T03:30:00.000Z',
title: 'Noir', title: 'Noir',
description: description:
'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir', 'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir',
icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg' icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ content: '', channel, date }) const result = parser({ content: '', channel, date })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,96 +1,96 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'entertainment.ie', site: 'entertainment.ie',
days: 2, days: 2,
url: function ({ date, channel }) { url: function ({ date, channel }) {
return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format(
'DD-MM-YYYY' 'DD-MM-YYYY'
)}&time=all-day` )}&time=all-day`
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
if (!start) return if (!start) return
if (prev && start < prev.start) { if (prev && start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
} }
const duration = parseDuration($item) const duration = parseDuration($item)
const stop = start.plus({ minutes: duration }) const stop = start.plus({ minutes: duration })
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
description: parseDescription($item), description: parseDescription($item),
categories: parseCategories($item), categories: parseCategories($item),
image: parseImage($item), image: parseImage($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get('https://entertainment.ie/tv/all-channels/') .get('https://entertainment.ie/tv/all-channels/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(data) const $ = cheerio.load(data)
let channels = $('.tv-filter-container > tv-filter').attr(':channels') let channels = $('.tv-filter-container > tv-filter').attr(':channels')
channels = JSON.parse(channels) channels = JSON.parse(channels)
return channels.map(c => { return channels.map(c => {
return { return {
lang: 'en', lang: 'en',
site_id: c.slug, site_id: c.slug,
name: c.name name: c.name
} }
}) })
} }
} }
function parseImage($item) { function parseImage($item) {
return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img')
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.text-holder h3').text().trim() return $item('.text-holder h3').text().trim()
} }
function parseDescription($item) { function parseDescription($item) {
return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description')
} }
function parseCategories($item) { function parseCategories($item) {
const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres')
return genres ? genres.split(', ') : [] return genres ? genres.split(', ') : []
} }
function parseStart($item, date) { function parseStart($item, date) {
let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time')
let [, time] = d ? d.split(', ') : [null, null] let [, time] = d ? d.split(', ') : [null, null]
return time return time
? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'UTC' zone: 'UTC'
}).toUTC() }).toUTC()
: null : null
} }
function parseDuration($item) { function parseDuration($item) {
const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration')
return parseInt(duration) return parseInt(duration)
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('.info-list > li').toArray() return $('.info-list > li').toArray()
} }

View File

@@ -1,58 +1,58 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const { parser, url } = require('./entertainment.ie.config.js') const { parser, url } = require('./entertainment.ie.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ date, content }).map(p => { const results = parser({ date, content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(51) expect(results.length).toBe(51)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-06-29T06:00:00.000Z', start: '2023-06-29T06:00:00.000Z',
stop: '2023-06-29T08:00:00.000Z', stop: '2023-06-29T08:00:00.000Z',
title: 'EuroNews', title: 'EuroNews',
description: 'European and international headlines live via satellite', description: 'European and international headlines live via satellite',
image: image:
'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg',
categories: ['Factual'] categories: ['Factual']
}) })
expect(results[50]).toMatchObject({ expect(results[50]).toMatchObject({
start: '2023-06-30T02:25:00.000Z', start: '2023-06-30T02:25:00.000Z',
stop: '2023-06-30T06:00:00.000Z', stop: '2023-06-30T06:00:00.000Z',
title: 'EuroNews', title: 'EuroNews',
description: 'European and international headlines live via satellite', description: 'European and international headlines live via satellite',
image: image:
'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg',
categories: ['Factual'] categories: ['Factual']
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html'))
const result = parser({ const result = parser({
date, date,
channel, channel,
content content
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,45 +1,45 @@
const axios = require('axios') const axios = require('axios')
const parser = require('epg-parser') const parser = require('epg-parser')
module.exports = { module.exports = {
site: 'epg.112114.xyz', site: 'epg.112114.xyz',
days: 1, days: 1,
url: 'https://epg.112114.xyz/pp.xml', url: 'https://epg.112114.xyz/pp.xml',
request: { request: {
cache: { cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
} }
}, },
parser: function ({ content, channel, date }) { parser: function ({ content, channel, date }) {
let programs = [] let programs = []
const items = parseItems(content, channel, date) const items = parseItems(content, channel, date)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title?.[0]?.value, title: item.title?.[0]?.value,
start: item.start, start: item.start,
stop: item.stop stop: item.stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get('https://epg.112114.xyz/pp.xml') .get('https://epg.112114.xyz/pp.xml')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const { channels } = parser.parse(data) const { channels } = parser.parse(data)
return channels.map(channel => ({ return channels.map(channel => ({
lang: 'zh', lang: 'zh',
site_id: channel.id, site_id: channel.id,
name: channel.displayName[0].value name: channel.displayName[0].value
})) }))
} }
} }
function parseItems(content, channel, date) { function parseItems(content, channel, date) {
const { programs } = parser.parse(content) const { programs } = parser.parse(content)
return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day'))
} }

View File

@@ -1,42 +1,42 @@
const { parser, url } = require('./epg.112114.xyz.config.js') const { parser, url } = require('./epg.112114.xyz.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const date = dayjs.utc('2025-01-11', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-11', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'BTV文艺', xmltv_id: 'BRTVArtsChannel.cn', lang: 'zh' } const channel = { site_id: 'BTV文艺', xmltv_id: 'BRTVArtsChannel.cn', lang: 'zh' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://epg.112114.xyz/pp.xml') expect(url).toBe('https://epg.112114.xyz/pp.xml')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
const results = parser({ date, content, channel }) const results = parser({ date, content, channel })
expect(results.length).toBe(28) expect(results.length).toBe(28)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-11T00:07:00.000Z', start: '2025-01-11T00:07:00.000Z',
stop: '2025-01-11T00:24:00.000Z', stop: '2025-01-11T00:24:00.000Z',
title: '每日文艺播报' title: '每日文艺播报'
}) })
expect(results[27]).toMatchObject({ expect(results[27]).toMatchObject({
start: '2025-01-11T15:16:00.000Z', start: '2025-01-11T15:16:00.000Z',
stop: '2025-01-11T15:59:00.000Z', stop: '2025-01-11T15:59:00.000Z',
title: '笑动剧场' title: '笑动剧场'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '' content: ''
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,64 +1,64 @@
const axios = require('axios') const axios = require('axios')
const iconv = require('iconv-lite') const iconv = require('iconv-lite')
const parser = require('epg-parser') const parser = require('epg-parser')
const { ungzip } = require('pako') const { ungzip } = require('pako')
let cachedContent let cachedContent
module.exports = { module.exports = {
site: 'epg.iptvx.one', site: 'epg.iptvx.one',
days: 2, days: 2,
url: 'https://iptvx.one/epg/epg_noarch.xml.gz', url: 'https://iptvx.one/epg/epg_noarch.xml.gz',
request: { request: {
maxContentLength: 500000000, // 500 MB maxContentLength: 500000000, // 500 MB
cache: { cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
} }
}, },
parser: function ({ buffer, channel, date, cached }) { parser: function ({ buffer, channel, date, cached }) {
if (!cached) cachedContent = undefined if (!cached) cachedContent = undefined
let programs = [] let programs = []
const items = parseItems(buffer, channel, date) const items = parseItems(buffer, channel, date)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title?.[0]?.value, title: item.title?.[0]?.value,
description: item.desc?.[0]?.value, description: item.desc?.[0]?.value,
start: item.start, start: item.start,
stop: item.stop stop: item.stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get('https://epg.iptvx.one/api/channels.json') .get('https://epg.iptvx.one/api/channels.json')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(channel => { return data.channels.map(channel => {
const [name] = channel.chan_names.split(' • ') const [name] = channel.chan_names.split(' • ')
return { return {
lang: 'ru', lang: 'ru',
site_id: channel.chan_id, site_id: channel.chan_id,
name name
} }
}) })
} }
} }
function parseItems(buffer, channel, date) { function parseItems(buffer, channel, date) {
if (!buffer) return [] if (!buffer) return []
if (!cachedContent) { if (!cachedContent) {
const content = ungzip(buffer) const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8') const encoded = iconv.decode(content, 'utf8')
cachedContent = parser.parse(encoded) cachedContent = parser.parse(encoded)
} }
const { programs } = cachedContent const { programs } = cachedContent
return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day'))
} }

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./epg.iptvx.one.config.js') const { parser, url } = require('./epg.iptvx.one.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '12-omsk', xmltv_id: 'Channel12.ru' } const channel = { site_id: '12-omsk', xmltv_id: 'Channel12.ru' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://iptvx.one/epg/epg_noarch.xml.gz') expect(url).toBe('https://iptvx.one/epg/epg_noarch.xml.gz')
}) })
it('can parse response', () => { it('can parse response', () => {
const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz')) const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz'))
const results = parser({ date, buffer, channel }) const results = parser({ date, buffer, channel })
expect(results.length).toBe(29) expect(results.length).toBe(29)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-13T00:00:00.000Z', start: '2025-01-13T00:00:00.000Z',
stop: '2025-01-13T00:55:00.000Z', stop: '2025-01-13T00:55:00.000Z',
title: 'Акценты недели', title: 'Акценты недели',
description: description:
'Программа расскажет зрителям о том, как развивались самые яркие события недели, поможет расставить акценты над самыми обсуждаемыми новостями. Россия, ток-шоу' 'Программа расскажет зрителям о том, как развивались самые яркие события недели, поможет расставить акценты над самыми обсуждаемыми новостями. Россия, ток-шоу'
}) })
expect(results[28]).toMatchObject({ expect(results[28]).toMatchObject({
start: '2025-01-13T22:15:00.000Z', start: '2025-01-13T22:15:00.000Z',
stop: '2025-01-14T00:00:00.000Z', stop: '2025-01-14T00:00:00.000Z',
title: 'д/с Необыкновенные люди', title: 'д/с Необыкновенные люди',
description: description:
'Герои цикла врачи, спортсмены, представители творческих профессий, волонтеры и многие-многие другие. Их деятельность связана с жизнью особенных людей. Россия, док. сериал' 'Герои цикла врачи, спортсмены, представители творческих профессий, волонтеры и многие-многие другие. Их деятельность связана с жизнью особенных людей. Россия, док. сериал'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
buffer: '' buffer: ''
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,100 +1,100 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
const BASIC_TOKEN = const BASIC_TOKEN =
'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' 'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
let session let session
module.exports = { module.exports = {
site: 'epg.telemach.ba', site: 'epg.telemach.ba',
days: 3, days: 3,
url({ channel, date }) { url({ channel, date }) {
return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format( return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format(
'YYYY-MM-DDTHH:mm:ss-00:00' 'YYYY-MM-DDTHH:mm:ss-00:00'
)}&toTime=${date )}&toTime=${date
.add(1, 'days') .add(1, 'days')
.subtract(1, 's') .subtract(1, 's')
.format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=12&languageId=59&cid=${channel.site_id}` .format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=12&languageId=59&cid=${channel.site_id}`
}, },
request: { request: {
async headers() { async headers() {
if (!session) { if (!session) {
session = await loadSessionDetails() session = await loadSessionDetails()
if (!session || !session.access_token) return null if (!session || !session.access_token) return null
} }
return { return {
Authorization: `Bearer ${session.access_token}` Authorization: `Bearer ${session.access_token}`
} }
} }
}, },
parser({ content }) { parser({ content }) {
try { try {
const programs = [] const programs = []
const data = JSON.parse(content) const data = JSON.parse(content)
for (const channelId in data) { for (const channelId in data) {
if (Array.isArray(data[channelId])) { if (Array.isArray(data[channelId])) {
data[channelId].forEach(item => { data[channelId].forEach(item => {
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.shortDescription, description: item.shortDescription,
image: parseImage(item), image: parseImage(item),
season: item.seasonNumber, season: item.seasonNumber,
episode: item.episodeNumber, episode: item.episodeNumber,
start: dayjs(item.startTime), start: dayjs(item.startTime),
stop: dayjs(item.endTime) stop: dayjs(item.endTime)
}) })
}) })
} }
} }
return programs return programs
} catch { } catch {
return [] return []
} }
}, },
async channels() { async channels() {
const session = await loadSessionDetails() const session = await loadSessionDetails()
if (!session || !session.access_token) return null if (!session || !session.access_token) return null
const data = await axios const data = await axios
.get( .get(
'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=12&languageId=59&imageSize=L', 'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=12&languageId=59&imageSize=L',
{ {
headers: { headers: {
Authorization: `Bearer ${session.access_token}` Authorization: `Bearer ${session.access_token}`
} }
} }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data.map(item => ({ return data.map(item => ({
lang: 'hr', lang: 'hr',
site_id: item.id, site_id: item.id,
name: item.name name: item.name
})) }))
} }
} }
function parseImage(item) { function parseImage(item) {
const baseURL = 'https://images-web.ug-be.cdn.united.cloud' const baseURL = 'https://images-web.ug-be.cdn.united.cloud'
return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null
} }
function loadSessionDetails() { function loadSessionDetails() {
return axios return axios
.post( .post(
'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials', 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials',
{}, {},
{ {
headers: { headers: {
Authorization: `Basic ${BASIC_TOKEN}` Authorization: `Basic ${BASIC_TOKEN}`
} }
} }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
} }

View File

@@ -1,94 +1,94 @@
const { parser, url, request } = require('./epg.telemach.ba.config.js') const { parser, url, request } = require('./epg.telemach.ba.config.js')
const fs = require('fs') const fs = require('fs')
const axios = require('axios') const axios = require('axios')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
axios.post.mockImplementation((url, data, opts) => { axios.post.mockImplementation((url, data, opts) => {
if ( if (
url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' && url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' &&
JSON.stringify(opts.headers) === JSON.stringify(opts.headers) ===
JSON.stringify({ JSON.stringify({
Authorization: Authorization:
'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' 'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
}) })
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
}) })
} else { } else {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json')))
}) })
} }
}) })
const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '1607', site_id: '1607',
xmltv_id: 'N1HD.hr' xmltv_id: 'N1HD.hr'
} }
it('can generate valid url', async () => { it('can generate valid url', async () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=12&languageId=59&cid=1607' 'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=12&languageId=59&cid=1607'
) )
}) })
it('can generate valid request headers', async () => { it('can generate valid request headers', async () => {
const result = await request.headers() const result = await request.headers()
expect(result).toMatchObject({ expect(result).toMatchObject({
Authorization: Authorization:
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg' 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg'
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content }) let results = parser({ content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(35) expect(results.length).toBe(35)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-20T00:00:00.000Z', start: '2025-01-20T00:00:00.000Z',
stop: '2025-01-20T00:30:00.000Z', stop: '2025-01-20T00:30:00.000Z',
title: 'DW Euromaxx', title: 'DW Euromaxx',
description: description:
'Euromaxx je lifestyle Europe magazine, koji nam donosi zanimljivosti iz evropskih gradova, priče o načinu života ljudi i upoznaje nas sa njihovim kulturama.', 'Euromaxx je lifestyle Europe magazine, koji nam donosi zanimljivosti iz evropskih gradova, priče o načinu života ljudi i upoznaje nas sa njihovim kulturama.',
image: image:
'https://images-web.ug-be.cdn.united.cloud/2021/02/18/06/05/21/stb_xl_cd4f72e01d308ecce782e29b69af7de6707b9e85.jpg', 'https://images-web.ug-be.cdn.united.cloud/2021/02/18/06/05/21/stb_xl_cd4f72e01d308ecce782e29b69af7de6707b9e85.jpg',
season: null, season: null,
episode: null episode: null
}) })
expect(results[34]).toMatchObject({ expect(results[34]).toMatchObject({
start: '2025-01-20T23:50:00.000Z', start: '2025-01-20T23:50:00.000Z',
stop: '2025-01-21T00:00:00.000Z', stop: '2025-01-21T00:00:00.000Z',
title: 'DW Shift', title: 'DW Shift',
description: 'Tjedni magazin koji nam donosi najnovije vijesti vezane za Internet.', description: 'Tjedni magazin koji nam donosi najnovije vijesti vezane za Internet.',
image: image:
'https://images-web.ug-be.cdn.united.cloud/2023/06/09/13/07/53/stb_xl_0849d5d70c1337651b85b6335e340e15bd5d6a73_340fc454bc73019d052cf936ebee5da3.jpg', 'https://images-web.ug-be.cdn.united.cloud/2023/06/09/13/07/53/stb_xl_0849d5d70c1337651b85b6335e340e15bd5d6a73_340fc454bc73019d052cf936ebee5da3.jpg',
season: null, season: null,
episode: null episode: null
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,101 +1,101 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
const BASIC_TOKEN = const BASIC_TOKEN =
'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' 'MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
let session let session
module.exports = { module.exports = {
site: 'epg.telemach.me', site: 'epg.telemach.me',
days: 3, days: 3,
url({ channel, date }) { url({ channel, date }) {
return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format( return `https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=${date.format(
'YYYY-MM-DDTHH:mm:ss-00:00' 'YYYY-MM-DDTHH:mm:ss-00:00'
)}&toTime=${date )}&toTime=${date
.add(1, 'days') .add(1, 'days')
.subtract(1, 's') .subtract(1, 's')
.format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=5&languageId=10001&cid=${channel.site_id}` .format('YYYY-MM-DDTHH:mm:ss-00:00')}&communityId=5&languageId=10001&cid=${channel.site_id}`
}, },
request: { request: {
async headers() { async headers() {
if (!session) { if (!session) {
session = await loadSessionDetails() session = await loadSessionDetails()
if (!session || !session.access_token) return null if (!session || !session.access_token) return null
} }
return { return {
Authorization: `Bearer ${session.access_token}`, Authorization: `Bearer ${session.access_token}`,
Referer: 'https://epg.telemach.me/' Referer: 'https://epg.telemach.me/'
} }
} }
}, },
parser({ content }) { parser({ content }) {
try { try {
const programs = [] const programs = []
const data = JSON.parse(content) const data = JSON.parse(content)
for (const channelId in data) { for (const channelId in data) {
if (Array.isArray(data[channelId])) { if (Array.isArray(data[channelId])) {
data[channelId].forEach(item => { data[channelId].forEach(item => {
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.shortDescription, description: item.shortDescription,
image: parseImage(item), image: parseImage(item),
season: item.seasonNumber, season: item.seasonNumber,
episode: item.episodeNumber, episode: item.episodeNumber,
start: dayjs(item.startTime), start: dayjs(item.startTime),
stop: dayjs(item.endTime) stop: dayjs(item.endTime)
}) })
}) })
} }
} }
return programs return programs
} catch { } catch {
return [] return []
} }
}, },
async channels() { async channels() {
const session = await loadSessionDetails() const session = await loadSessionDetails()
if (!session || !session.access_token) return null if (!session || !session.access_token) return null
const data = await axios const data = await axios
.get( .get(
'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=5&languageId=10001&imageSize=L', 'https://api-web.ug-be.cdn.united.cloud/v1/public/channels?channelType=TV&communityId=5&languageId=10001&imageSize=L',
{ {
headers: { headers: {
Authorization: `Bearer ${session.access_token}` Authorization: `Bearer ${session.access_token}`
} }
} }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data.map(item => ({ return data.map(item => ({
lang: 'bs', lang: 'bs',
site_id: item.id, site_id: item.id,
name: item.name name: item.name
})) }))
} }
} }
function parseImage(item) { function parseImage(item) {
const baseURL = 'https://images-web.ug-be.cdn.united.cloud' const baseURL = 'https://images-web.ug-be.cdn.united.cloud'
return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null return Array.isArray(item?.images) && item.images[0] ? `${baseURL}${item.images[0].path}` : null
} }
function loadSessionDetails() { function loadSessionDetails() {
return axios return axios
.post( .post(
'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials', 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials',
{}, {},
{ {
headers: { headers: {
Authorization: `Basic ${BASIC_TOKEN}` Authorization: `Basic ${BASIC_TOKEN}`
} }
} }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
} }

View File

@@ -1,96 +1,96 @@
const { parser, url, request } = require('./epg.telemach.me.config.js') const { parser, url, request } = require('./epg.telemach.me.config.js')
const fs = require('fs') const fs = require('fs')
const axios = require('axios') const axios = require('axios')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
axios.post.mockImplementation((url, data, opts) => { axios.post.mockImplementation((url, data, opts) => {
if ( if (
url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' && url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' &&
JSON.stringify(opts.headers) === JSON.stringify(opts.headers) ===
JSON.stringify({ JSON.stringify({
Authorization: Authorization:
'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc=' 'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
}) })
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
}) })
} else { } else {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json')))
}) })
} }
}) })
const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-20', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '92', site_id: '92',
xmltv_id: 'PinkKids.rs' xmltv_id: 'PinkKids.rs'
} }
it('can generate valid url', async () => { it('can generate valid url', async () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=5&languageId=10001&cid=92' 'https://api-web.ug-be.cdn.united.cloud/v1/public/events/epg?fromTime=2025-01-20T00:00:00-00:00&toTime=2025-01-20T23:59:59-00:00&communityId=5&languageId=10001&cid=92'
) )
}) })
it('can generate valid request headers', async () => { it('can generate valid request headers', async () => {
const result = await request.headers() const result = await request.headers()
expect(result).toMatchObject({ expect(result).toMatchObject({
Authorization: Authorization:
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg', 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg',
Referer: 'https://epg.telemach.me/' Referer: 'https://epg.telemach.me/'
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content }) let results = parser({ content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(55) expect(results.length).toBe(55)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-19T23:20:00.000Z', start: '2025-01-19T23:20:00.000Z',
stop: '2025-01-20T00:10:00.000Z', stop: '2025-01-20T00:10:00.000Z',
title: 'Pinkove Zvezdice', title: 'Pinkove Zvezdice',
description: description:
'Četvrta sezona najgledanijeg dečijeg muzičkog takmičenja, "Pinkove zvezdice" došlo do promena, pa će tako gledaoci imati priliku da najtalentovaniju decu gledaju na novoj, spektakularnoj sceni. Nova...', 'Četvrta sezona najgledanijeg dečijeg muzičkog takmičenja, "Pinkove zvezdice" došlo do promena, pa će tako gledaoci imati priliku da najtalentovaniju decu gledaju na novoj, spektakularnoj sceni. Nova...',
image: image:
'https://images-web.ug-be.cdn.united.cloud/2023/06/22/11/19/19/stb_xl_115752ec1e05872b86ceda7726d347f533e17f43_340fc454bc73019d052cf936ebee5da3.jpg', 'https://images-web.ug-be.cdn.united.cloud/2023/06/22/11/19/19/stb_xl_115752ec1e05872b86ceda7726d347f533e17f43_340fc454bc73019d052cf936ebee5da3.jpg',
season: null, season: null,
episode: null episode: null
}) })
expect(results[54]).toMatchObject({ expect(results[54]).toMatchObject({
start: '2025-01-20T23:50:00.000Z', start: '2025-01-20T23:50:00.000Z',
stop: '2025-01-21T00:10:00.000Z', stop: '2025-01-21T00:10:00.000Z',
title: 'Hajdi', title: 'Hajdi',
description: description:
'Život nekada nije jednostavan. To najbolje zna Hajdi. Nakon što je ostala siroče, njena tetka je odvodi visoko u Alpe kod njenog dede. Ona uz nove prijatelje i dedu uskoro zavoli svoj novi život. Ipak...', 'Život nekada nije jednostavan. To najbolje zna Hajdi. Nakon što je ostala siroče, njena tetka je odvodi visoko u Alpe kod njenog dede. Ona uz nove prijatelje i dedu uskoro zavoli svoj novi život. Ipak...',
image: image:
'https://images-web.ug-be.cdn.united.cloud/2024/05/10/14/49/09/stb_xl_7d1c73ee4df7de5c4157e9daccae098d50ee853d_99230e7f5bdc95451f37aa31f8425b4d.jpg', 'https://images-web.ug-be.cdn.united.cloud/2024/05/10/14/49/09/stb_xl_7d1c73ee4df7de5c4157e9daccae098d50ee853d_99230e7f5bdc95451f37aa31f8425b4d.jpg',
season: null, season: null,
episode: null episode: null
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,45 +1,45 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const TOKEN = '1610283054' const TOKEN = '1610283054'
module.exports = { module.exports = {
site: 'epgmaster.com', site: 'epgmaster.com',
url({ channel }) { url({ channel }) {
return `https://epgmaster.com/api/channels/${channel.site_id}/epgs?token=${TOKEN}` return `https://epgmaster.com/api/channels/${channel.site_id}/epgs?token=${TOKEN}`
}, },
parser({ content, date }) { parser({ content, date }) {
return parseItems(content, date).map(item => { return parseItems(content, date).map(item => {
return { return {
title: item.programName, title: item.programName,
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
} }
}) })
} }
} }
function parseStart(item) { function parseStart(item) {
return dayjs.utc(`${item.startDate} ${item.startTime}`, 'YYYY-MM-DD HH:mm:ss') return dayjs.utc(`${item.startDate} ${item.startTime}`, 'YYYY-MM-DD HH:mm:ss')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.utc(`${item.startDate} ${item.endTime}`, 'YYYY-MM-DD HH:mm:ss') return dayjs.utc(`${item.startDate} ${item.endTime}`, 'YYYY-MM-DD HH:mm:ss')
} }
function parseItems(content, date) { function parseItems(content, date) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data)) return [] if (!data || !Array.isArray(data)) return []
const filtered = data.find(group => date.format('YYYY-MM-DD') === group.date) const filtered = data.find(group => date.format('YYYY-MM-DD') === group.date)
if (!filtered || !Array.isArray(filtered.epgTokenList)) return [] if (!filtered || !Array.isArray(filtered.epgTokenList)) return []
return filtered.epgTokenList return filtered.epgTokenList
} catch { } catch {
return [] return []
} }
} }

View File

@@ -1,45 +1,45 @@
const { parser, url } = require('./epgmaster.com.config.js') const { parser, url } = require('./epgmaster.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-05-18', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-05-18', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'ntv' } const channel = { site_id: 'ntv' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://epgmaster.com/api/channels/ntv/epgs?token=1610283054') expect(url({ channel })).toBe('https://epgmaster.com/api/channels/ntv/epgs?token=1610283054')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(46) expect(results.length).toBe(46)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'Krishi Teleflim-Bharosa Yuwama', title: 'Krishi Teleflim-Bharosa Yuwama',
start: '2025-05-18T00:00:00.000Z', start: '2025-05-18T00:00:00.000Z',
stop: '2025-05-18T00:15:00.000Z' stop: '2025-05-18T00:15:00.000Z'
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
title: 'News in Nepali [Rec.]', title: 'News in Nepali [Rec.]',
start: '2025-05-18T00:15:00.000Z', start: '2025-05-18T00:15:00.000Z',
stop: '2025-05-18T00:45:00.000Z' stop: '2025-05-18T00:45:00.000Z'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: '', date }) const results = parser({ content: '', date })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,75 +1,75 @@
const axios = require('axios') const axios = require('axios')
const iconv = require('iconv-lite') const iconv = require('iconv-lite')
const parser = require('epg-parser') const parser = require('epg-parser')
const { ungzip } = require('pako') const { ungzip } = require('pako')
let cachedContent let cachedContent
module.exports = { module.exports = {
site: 'epgshare01.online', site: 'epgshare01.online',
days: 2, days: 2,
url({ channel }) { url({ channel }) {
const [tag] = channel.site_id.split('#') const [tag] = channel.site_id.split('#')
return `https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz` return `https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`
}, },
request: { request: {
cache: { cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
}, },
maxContentLength: 100000000 // 100 MB maxContentLength: 100000000 // 100 MB
}, },
parser({ buffer, channel, date, cached }) { parser({ buffer, channel, date, cached }) {
if (!cached) cachedContent = undefined if (!cached) cachedContent = undefined
let programs = [] let programs = []
const items = parseItems(buffer, channel, date) const items = parseItems(buffer, channel, date)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title?.[0]?.value, title: item.title?.[0]?.value,
description: item.desc?.[0]?.value, description: item.desc?.[0]?.value,
start: item.start, start: item.start,
stop: item.stop stop: item.stop
}) })
}) })
return programs return programs
}, },
async channels({ tag }) { async channels({ tag }) {
const buffer = await axios const buffer = await axios
.get(`https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`, { .get(`https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`, {
responseType: 'arraybuffer' responseType: 'arraybuffer'
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
const content = ungzip(buffer) const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8') const encoded = iconv.decode(content, 'utf8')
const { channels } = parser.parse(encoded) const { channels } = parser.parse(encoded)
return channels.map(channel => { return channels.map(channel => {
const displayName = channel.displayName[0] const displayName = channel.displayName[0]
return { return {
lang: displayName.lang || 'en', lang: displayName.lang || 'en',
site_id: `${tag}#${channel.id}`, site_id: `${tag}#${channel.id}`,
name: displayName.value name: displayName.value
} }
}) })
} }
} }
function parseItems(buffer, channel, date) { function parseItems(buffer, channel, date) {
if (!buffer) return [] if (!buffer) return []
if (!cachedContent) { if (!cachedContent) {
const content = ungzip(buffer) const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8') const encoded = iconv.decode(content, 'utf8')
cachedContent = parser.parse(encoded) cachedContent = parser.parse(encoded)
} }
const { programs } = cachedContent const { programs } = cachedContent
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day')) return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day'))
} }

View File

@@ -1,43 +1,43 @@
const { parser, url } = require('./epgshare01.online.config.js') const { parser, url } = require('./epgshare01.online.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-02-09', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-02-09', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'ALJAZEERA1#AlJazeera.English.net' } const channel = { site_id: 'ALJAZEERA1#AlJazeera.English.net' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://epgshare01.online/epgshare01/epg_ripper_ALJAZEERA1.xml.gz') expect(url({ channel })).toBe('https://epgshare01.online/epgshare01/epg_ripper_ALJAZEERA1.xml.gz')
}) })
it('can parse response', () => { it('can parse response', () => {
const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz')) const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz'))
const results = parser({ buffer, channel, date, cached: false }) const results = parser({ buffer, channel, date, cached: false })
expect(results.length).toBe(40) expect(results.length).toBe(40)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'The Palestine Laboratory', title: 'The Palestine Laboratory',
description: description:
"Exposing how Israel's sales of military technology is aiding state control around the world.", "Exposing how Israel's sales of military technology is aiding state control around the world.",
start: '2025-02-09T00:00:00.000Z', start: '2025-02-09T00:00:00.000Z',
stop: '2025-02-09T01:00:00.000Z' stop: '2025-02-09T01:00:00.000Z'
}) })
expect(results[39]).toMatchObject({ expect(results[39]).toMatchObject({
title: 'Inside Story', title: 'Inside Story',
description: description:
'Beyond the headlines to the heart of the news of the day. Al Jazeera gets the Inside Story from some of the best minds from around the globe.', 'Beyond the headlines to the heart of the news of the day. Al Jazeera gets the Inside Story from some of the best minds from around the globe.',
start: '2025-02-09T23:30:00.000Z', start: '2025-02-09T23:30:00.000Z',
stop: '2025-02-10T00:00:00.000Z' stop: '2025-02-10T00:00:00.000Z'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: '', channel, date, cached: false }) const results = parser({ content: '', channel, date, cached: false })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,102 +1,102 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'firstmedia.com', site: 'firstmedia.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://api.firstmedia.com/api/content/tv-guide/list?date=${date.format( return `https://api.firstmedia.com/api/content/tv-guide/list?date=${date.format(
'DD/MM/YYYY' 'DD/MM/YYYY'
)}&channel=${channel.site_id}&startTime=1&endTime=24` )}&channel=${channel.site_id}&startTime=1&endTime=24`
}, },
parser({ content, channel, date }) { parser({ content, channel, date }) {
if (!content || !channel || !date) return [] if (!content || !channel || !date) return []
const programs = [] const programs = []
const items = parseItems(content, channel.site_id) const items = parseItems(content, channel.site_id)
.map(item => { .map(item => {
item.start = toDelta(item.date, item.startTime) item.start = toDelta(item.date, item.startTime)
item.stop = toDelta(item.date, item.endTime) item.stop = toDelta(item.date, item.endTime)
return item return item
}) })
.sort((a, b) => a.start - b.start) .sort((a, b) => a.start - b.start)
const dt = date.tz('Asia/Jakarta').startOf('d') const dt = date.tz('Asia/Jakarta').startOf('d')
let lastStop let lastStop
items.forEach(item => { items.forEach(item => {
if (lastStop === undefined || item.start >= lastStop) { if (lastStop === undefined || item.start >= lastStop) {
lastStop = item.stop lastStop = item.stop
programs.push({ programs.push({
title: parseTitle(item), title: parseTitle(item),
description: parseDescription(item), description: parseDescription(item),
start: asDate(parseStart({ item, date: dt })), start: asDate(parseStart({ item, date: dt })),
stop: asDate(parseStop({ item, date: dt })) stop: asDate(parseStop({ item, date: dt }))
}) })
} }
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const result = await axios const result = await axios
.get( .get(
`https://api.firstmedia.com/api/content/tv-guide/list?date=${dayjs().format( `https://api.firstmedia.com/api/content/tv-guide/list?date=${dayjs().format(
'DD/MM/YYYY' 'DD/MM/YYYY'
)}&channel=&startTime=0&endTime=24` )}&channel=&startTime=0&endTime=24`
) )
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
const channels = [] const channels = []
if (result.data && result.data.entries) { if (result.data && result.data.entries) {
Object.values(result.data.entries).forEach(schedules => { Object.values(result.data.entries).forEach(schedules => {
if (schedules.length) { if (schedules.length) {
channels.push({ channels.push({
lang: 'en', lang: 'en',
site_id: schedules[0].channel.no, site_id: schedules[0].channel.no,
name: schedules[0].channel.name name: schedules[0].channel.name
}) })
} }
}) })
} }
return channels return channels
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
return JSON.parse(content.trim()).data.entries[channel] || [] return JSON.parse(content.trim()).data.entries[channel] || []
} }
function parseTitle(item) { function parseTitle(item) {
return item.title return item.title
} }
function parseDescription(item) { function parseDescription(item) {
return item.long_description return item.long_description
} }
function parseStart({ item, date }) { function parseStart({ item, date }) {
return date.add(item.start, 'ms') return date.add(item.start, 'ms')
} }
function parseStop({ item, date }) { function parseStop({ item, date }) {
return date.add(item.stop, 'ms') return date.add(item.stop, 'ms')
} }
function toDelta(from, to) { function toDelta(from, to) {
return toDate(to).diff(toDate(from), 'milliseconds') return toDate(to).diff(toDate(from), 'milliseconds')
} }
function toDate(date) { function toDate(date) {
return dayjs(date, 'YYYY-MM-DD HH:mm:ss') return dayjs(date, 'YYYY-MM-DD HH:mm:ss')
} }
function asDate(date) { function asDate(date) {
return date.toISOString() return date.toISOString()
} }

View File

@@ -1,69 +1,69 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'foxsports.com.au', site: 'foxsports.com.au',
days: 3, days: 3,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ date }) { url({ date }) {
return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format( return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&to=${date.add(1, 'd').format('YYYY-MM-DD')}` )}&to=${date.add(1, 'd').format('YYYY-MM-DD')}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.programmeTitle, title: item.programmeTitle,
sub_title: item.title, sub_title: item.title,
category: item.genreTitle, category: item.genreTitle,
description: item.synopsis, description: item.synopsis,
start: dayjs.utc(item.startTime), start: dayjs.utc(item.startTime),
stop: dayjs.utc(item.endTime) stop: dayjs.utc(item.endTime)
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get( .get(
`https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${dayjs().format( `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${dayjs().format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&to=${dayjs().add(1, 'd').format('YYYY-MM-DD')}` )}&to=${dayjs().add(1, 'd').format('YYYY-MM-DD')}`
) )
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
let channels = {} let channels = {}
data['channel-programme'].forEach(item => { data['channel-programme'].forEach(item => {
if (channels[item.channelId]) return if (channels[item.channelId]) return
channels[item.channelId] = { channels[item.channelId] = {
lang: 'en', lang: 'en',
site_id: item.channelId, site_id: item.channelId,
name: item.channelName name: item.channelName
} }
}) })
return Object.values(channels) return Object.values(channels)
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data) return [] if (!data) return []
const programmes = data['channel-programme'] const programmes = data['channel-programme']
if (!Array.isArray(programmes)) return [] if (!Array.isArray(programmes)) return []
const channelData = programmes.filter(i => i.channelId == channel.site_id) const channelData = programmes.filter(i => i.channelId == channel.site_id)
return channelData && Array.isArray(channelData) ? channelData : [] return channelData && Array.isArray(channelData) ? channelData : []
} }

View File

@@ -1,138 +1,138 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const cheerio = require('cheerio') const cheerio = require('cheerio')
module.exports = { module.exports = {
site: 'foxtel.com.au', site: 'foxtel.com.au',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format( return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format(
'YYYY/MM/DD' 'YYYY/MM/DD'
)}` )}`
}, },
request: { request: {
headers: { headers: {
'Accept-Language': 'en-US,en;', 'Accept-Language': 'en-US,en;',
Cookie: 'AAMC_foxtel_0=REGION|7', Cookie: 'AAMC_foxtel_0=REGION|7',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
} }
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
for (let item of items) { for (let item of items) {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item) let start = parseStart($item)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
sub_title: parseSubTitle($item), sub_title: parseSubTitle($item),
image: parseImage($item), image: parseImage($item),
rating: parseRating($item), rating: parseRating($item),
season: parseSeason($item), season: parseSeason($item),
episode: parseEpisode($item), episode: parseEpisode($item),
start, start,
stop stop
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', { .get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', {
headers: { headers: {
'User-Agent': 'insomnia/2022.7.5' 'User-Agent': 'insomnia/2022.7.5'
} }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
const slug = item.name const slug = item.name
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/&/g, '') .replace(/&/g, '')
.replace(/[^a-z0-9\s]/gi, '') .replace(/[^a-z0-9\s]/gi, '')
.replace(/\s/g, '-') .replace(/\s/g, '-')
return { return {
lang: 'en', lang: 'en',
name: item.name, name: item.name,
site_id: `${slug}/${item.channelTag}` site_id: `${slug}/${item.channelTag}`
} }
}) })
} }
} }
function parseSeason($item) { function parseSeason($item) {
let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title') let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title')
if (!seasonString) return null if (!seasonString) return null
let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null] let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null]
return season ? parseInt(season) : null return season ? parseInt(season) : null
} }
function parseEpisode($item) { function parseEpisode($item) {
let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title') let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title')
if (!episodeString) return null if (!episodeString) return null
let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null] let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null]
return episode ? parseInt(episode) : null return episode ? parseInt(episode) : null
} }
function parseImage($item) { function parseImage($item) {
return $item('.epg-event-thumbnail > img').attr('src') return $item('.epg-event-thumbnail > img').attr('src')
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.epg-event-description').clone().children().remove().end().text().trim() return $item('.epg-event-description').clone().children().remove().end().text().trim()
} }
function parseSubTitle($item) { function parseSubTitle($item) {
let subtitle = $item('.epg-event-description > div') let subtitle = $item('.epg-event-description > div')
.clone() .clone()
.children() .children()
.remove() .remove()
.end() .end()
.text() .text()
.trim() .trim()
.split(',') .split(',')
subtitle = subtitle.pop() subtitle = subtitle.pop()
const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null]
return subtitle.replace(`(${rating})`, '').trim() return subtitle.replace(`(${rating})`, '').trim()
} }
function parseRating($item) { function parseRating($item) {
const subtitle = $item('.epg-event-description > div').text().trim() const subtitle = $item('.epg-event-description > div').text().trim()
const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null]
return rating return rating
? { ? {
system: 'ACB', system: 'ACB',
value: rating value: rating
} }
: null : null
} }
function parseStart($item) { function parseStart($item) {
const unix = $item('*').data('scheduled-date') const unix = $item('*').data('scheduled-date')
return dayjs(parseInt(unix)) return dayjs(parseInt(unix))
} }
function parseItems(content) { function parseItems(content) {
if (!content) return [] if (!content) return []
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('#epg-channel-events > a').toArray() return $('#epg-channel-events > a').toArray()
} }

View File

@@ -1,60 +1,60 @@
const { parser, url, request } = require('./foxtel.com.au.config.js') const { parser, url, request } = require('./foxtel.com.au.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'Channel-9-Sydney/NIN', site_id: 'Channel-9-Sydney/NIN',
xmltv_id: 'Channel9Sydney.au' xmltv_id: 'Channel9Sydney.au'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08' 'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08'
) )
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Accept-Language': 'en-US,en;', 'Accept-Language': 'en-US,en;',
Cookie: 'AAMC_foxtel_0=REGION|7' Cookie: 'AAMC_foxtel_0=REGION|7'
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
let results = parser({ content }) let results = parser({ content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-11-07T12:40:00.000Z', start: '2022-11-07T12:40:00.000Z',
stop: '2022-11-07T13:30:00.000Z', stop: '2022-11-07T13:30:00.000Z',
title: 'The Equalizer', title: 'The Equalizer',
sub_title: 'Glory', sub_title: 'Glory',
image: image:
'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830', 'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830',
rating: { rating: {
system: 'ACB', system: 'ACB',
value: 'M' value: 'M'
}, },
season: 1, season: 1,
episode: 2 episode: 2
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,62 +1,62 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'freetv.tv', site: 'freetv.tv',
days: 2, days: 2,
url: function ({ channel, date }) { url: function ({ channel, date }) {
const localDate = dayjs(date).tz('Asia/Jerusalem') const localDate = dayjs(date).tz('Asia/Jerusalem')
const since = localDate.startOf('day').format('YYYY-MM-DDTHH:mmZZ') const since = localDate.startOf('day').format('YYYY-MM-DDTHH:mmZZ')
const till = localDate.add(1, 'day').startOf('day').format('YYYY-MM-DDTHH:mmZZ') const till = localDate.add(1, 'day').startOf('day').format('YYYY-MM-DDTHH:mmZZ')
return `https://web.freetv.tv/api/products/lives/programmes?liveId[]=${ return `https://web.freetv.tv/api/products/lives/programmes?liveId[]=${
channel.site_id channel.site_id
}&since=${encodeURIComponent(since)}&till=${encodeURIComponent(till)}&lang=HEB&platform=BROWSER` }&since=${encodeURIComponent(since)}&till=${encodeURIComponent(till)}&lang=HEB&platform=BROWSER`
}, },
parser: function ({ content }) { parser: function ({ content }) {
const programs = [] const programs = []
let items = [] let items = []
try { try {
items = JSON.parse(content) items = JSON.parse(content)
} catch { } catch {
return programs return programs
} }
items.forEach(item => { items.forEach(item => {
const start = parseStart(item) const start = parseStart(item)
const stop = parseStop(item) const stop = parseStop(item)
if (!start.isValid() || !stop.isValid()) return if (!start.isValid() || !stop.isValid()) return
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description || item.lead, description: item.description || item.lead,
image: getImageUrl(item), image: getImageUrl(item),
icon: getImageUrl(item), icon: getImageUrl(item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseStart(item) { function parseStart(item) {
return item.since ? dayjs.utc(item.since).tz('Asia/Jerusalem') : null return item.since ? dayjs.utc(item.since).tz('Asia/Jerusalem') : null
} }
function parseStop(item) { function parseStop(item) {
return item.till ? dayjs.utc(item.till).tz('Asia/Jerusalem') : null return item.till ? dayjs.utc(item.till).tz('Asia/Jerusalem') : null
} }
function getImageUrl(item) { function getImageUrl(item) {
const url = item.images?.['16x9']?.[0]?.url const url = item.images?.['16x9']?.[0]?.url
return url ? `https:${url}` : null return url ? `https:${url}` : null
} }

View File

@@ -1,73 +1,73 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const parseDuration = require('parse-duration').default const parseDuration = require('parse-duration').default
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'freeview.co.uk', site: 'freeview.co.uk',
days: 2, days: 2,
url({ date, channel }) { url({ date, channel }) {
const [networkId] = channel.site_id.split('#') const [networkId] = channel.site_id.split('#')
const startTimestamp = date.startOf('d').unix() const startTimestamp = date.startOf('d').unix()
return `https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}` return `https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
let items = parseItems(content, channel) let items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
const start = parseStart(item) const start = parseStart(item)
const duration = parseDuration(item.duration) const duration = parseDuration(item.duration)
const stop = start.add(duration, 'ms') const stop = start.add(duration, 'ms')
programs.push({ programs.push({
title: item.main_title, title: item.main_title,
subtitle: item.secondary_title, subtitle: item.secondary_title,
image: parseImage(item), image: parseImage(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const networkId = '64257' // Great London const networkId = '64257' // Great London
const startTimestamp = dayjs.utc().startOf('d').unix() const startTimestamp = dayjs.utc().startOf('d').unix()
const data = await axios const data = await axios
.get(`https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}`) .get(`https://www.freeview.co.uk/api/tv-guide?nid=${networkId}&start=${startTimestamp}`)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.data.programs.map(item => ({ return data.data.programs.map(item => ({
lang: 'en', lang: 'en',
site_id: `${networkId}#${item.service_id}`, site_id: `${networkId}#${item.service_id}`,
name: item.title name: item.title
})) }))
} }
} }
function parseImage(item) { function parseImage(item) {
return item.image_url ? `${item.image_url}?w=800` : null return item.image_url ? `${item.image_url}?w=800` : null
} }
function parseStart(item) { function parseStart(item) {
return dayjs(item.start_time) return dayjs(item.start_time)
} }
function parseItems(content, channel) { function parseItems(content, channel) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
const programs = data?.data?.programs const programs = data?.data?.programs
if (!Array.isArray(programs)) return [] if (!Array.isArray(programs)) return []
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const channelData = programs.find(p => p.service_id === channelId) const channelData = programs.find(p => p.service_id === channelId)
const channelPrograms = channelData?.events const channelPrograms = channelData?.events
if (!Array.isArray(channelPrograms)) return [] if (!Array.isArray(channelPrograms)) return []
return channelPrograms return channelPrograms
} catch { } catch {
return [] return []
} }
} }

View File

@@ -1,55 +1,55 @@
const { parser, url } = require('./freeview.co.uk.config.js') const { parser, url } = require('./freeview.co.uk.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-01-16', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-01-16', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '64257#4164', site_id: '64257#4164',
xmltv_id: 'BBCOneLondon.uk' xmltv_id: 'BBCOneLondon.uk'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://www.freeview.co.uk/api/tv-guide?nid=64257&start=1736985600' 'https://www.freeview.co.uk/api/tv-guide?nid=64257&start=1736985600'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel }) let results = parser({ content, channel })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(25) expect(results.length).toBe(25)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-16T00:00:00.000Z', start: '2025-01-16T00:00:00.000Z',
stop: '2025-01-16T00:45:00.000Z', stop: '2025-01-16T00:45:00.000Z',
title: 'The Weakest Link', title: 'The Weakest Link',
subtitle: 'Series 4: Episode 7', subtitle: 'Series 4: Episode 7',
image: 'https://img.freeviewplay.tv/p0b041486e4378cbf074511098f74e78f?w=800' image: 'https://img.freeviewplay.tv/p0b041486e4378cbf074511098f74e78f?w=800'
}) })
expect(results[24]).toMatchObject({ expect(results[24]).toMatchObject({
start: '2025-01-16T23:40:00.000Z', start: '2025-01-16T23:40:00.000Z',
stop: '2025-01-17T00:10:00.000Z', stop: '2025-01-17T00:10:00.000Z',
title: 'Newscast', title: 'Newscast',
subtitle: 'Series 5: 16/01/2025', subtitle: 'Series 5: 16/01/2025',
image: 'https://img.freeviewplay.tv/pb43e790fe10fe5ba668caf22224bc312?w=800' image: 'https://img.freeviewplay.tv/pb43e790fe10fe5ba668caf22224bc312?w=800'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
content: '[]', content: '[]',
channel channel
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

Some files were not shown because too many files have changed in this diff Show More