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

View File

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

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./abc.net.au.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'Sydney#ABC1' }
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(30)
expect(results[0]).toMatchObject({
title: "Julia Zemiro's Home Delivery",
sub_title: 'Maggie Beer',
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.",
category: ['Entertainment', 'Factual'],
rating: {
system: 'ACB',
value: 'G'
},
season: null,
episode: null,
image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg',
start: '2025-02-03T12:40:00.000Z',
stop: '2025-02-03T13:09:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')),
channel
})
expect(results).toMatchObject([])
})
const { parser, url } = require('./abc.net.au.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'Sydney#ABC1' }
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(30)
expect(results[0]).toMatchObject({
title: "Julia Zemiro's Home Delivery",
sub_title: 'Maggie Beer',
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.",
category: ['Entertainment', 'Factual'],
rating: {
system: 'ACB',
value: 'G'
},
season: null,
episode: null,
image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg',
start: '2025-02-03T12:40:00.000Z',
stop: '2025-02-03T13:09:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')),
channel
})
expect(results).toMatchObject([])
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,72 +1,72 @@
const { url, parser } = require('./arirang.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d')
const channel = {
xmltv_id: 'ArirangWorld.kr',
site_id: 'CH_W',
name: 'Arirang World',
lang: 'en',
logo: 'https://i.imgur.com/5Aoithj.png'
}
const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8')
const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8')
const context = { channel: channel, content: content, date: date }
it('can generate valid url', () => {
expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy')
})
it('can handle empty guide', async () => {
const results = await parser({ channel: channel, content: '', date: date })
expect(results).toMatchObject([])
})
it('can parse response', async () => {
axios.post.mockImplementation((url, data) => {
if (
url === 'https://www.arirang.com/v1.0/open/external/proxy' &&
JSON.stringify(data) ===
JSON.stringify({
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
method: 'POST',
headers: {},
body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } }
})
) {
return Promise.resolve({
data: JSON.parse(content)
})
} else if (
url === 'https://www.arirang.com/v1.0/open/program/detail' &&
JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' })
) {
return Promise.resolve({
data: JSON.parse(programDetail)
})
} else {
return Promise.resolve({
data: ''
})
}
})
const results = await parser(context)
expect(results[0]).toMatchObject({
title: 'Diplomat Archives: Hidden Stories',
start: dayjs.tz(date, 'Asia/Seoul'),
stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'),
image:
'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.',
category: 'Current Affairs'
})
const { url, parser } = require('./arirang.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.tz('2025-04-20', 'Asia/Seoul').startOf('d')
const channel = {
xmltv_id: 'ArirangWorld.kr',
site_id: 'CH_W',
name: 'Arirang World',
lang: 'en',
logo: 'https://i.imgur.com/5Aoithj.png'
}
const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8')
const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8')
const context = { channel: channel, content: content, date: date }
it('can generate valid url', () => {
expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy')
})
it('can handle empty guide', async () => {
const results = await parser({ channel: channel, content: '', date: date })
expect(results).toMatchObject([])
})
it('can parse response', async () => {
axios.post.mockImplementation((url, data) => {
if (
url === 'https://www.arirang.com/v1.0/open/external/proxy' &&
JSON.stringify(data) ===
JSON.stringify({
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
method: 'POST',
headers: {},
body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20250420', planNo: '1' } } }
})
) {
return Promise.resolve({
data: JSON.parse(content)
})
} else if (
url === 'https://www.arirang.com/v1.0/open/program/detail' &&
JSON.stringify(data) === JSON.stringify({ bis_program_code: '2025006T' })
) {
return Promise.resolve({
data: JSON.parse(programDetail)
})
} else {
return Promise.resolve({
data: ''
})
}
})
const results = await parser(context)
expect(results[0]).toMatchObject({
title: 'Diplomat Archives: Hidden Stories',
start: dayjs.tz(date, 'Asia/Seoul'),
stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'),
image:
'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.',
category: 'Current Affairs'
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +1,43 @@
const { parser, url } = require('./beinsports.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
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' }
it('can generate valid url', () => {
const result = url({ date, channel })
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'
)
})
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"}]}'
it('can parse response', () => {
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-10-20T23:30:00.000Z',
stop: '2023-10-21T01:00:00.000Z',
title: 'Tokyo Day 5 QF 2',
description: 'Exclusive coverage of the 2023 ATP Tour on beIN SPORTS'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./beinsports.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
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' }
it('can generate valid url', () => {
const result = url({ date, channel })
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'
)
})
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"}]}'
it('can parse response', () => {
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-10-20T23:30:00.000Z',
stop: '2023-10-21T01:00:00.000Z',
title: 'Tokyo Day 5 QF 2',
description: 'Exclusive coverage of the 2023 ATP Tour on beIN SPORTS'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,146 +1,146 @@
const { parser, url } = require('./canalplus.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: 'bi#198',
xmltv_id: 'CanalPlusCinemaFrance.fr'
}
it('can generate valid url for today', done => {
axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
})
} else {
return Promise.resolve({ data: '' })
}
})
const today = dayjs.utc().startOf('d')
url({ channel, date: today })
.then(result => {
expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0'
)
done()
})
.catch(done)
})
it('can generate valid url for tomorrow', done => {
axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
})
} else {
return Promise.resolve({ data: '' })
}
})
const tomorrow = dayjs.utc().startOf('d').add(1, 'd')
url({ channel, date: tomorrow })
.then(result => {
expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1'
)
done()
})
.catch(done)
})
it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => {
if (
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'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
})
} else if (
url ===
'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({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-01-12T06:28:00.000Z',
stop: '2023-01-12T12:06:00.000Z',
title: 'Le cercle',
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.",
image:
'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573',
presenter: ['Lily Bloom'],
rating: {
system: 'CSA',
value: '-10'
}
},
{
start: '2023-01-12T12:06:00.000Z',
stop: '2023-01-12T13:06:00.000Z',
title: 'Illusions perdues',
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...",
image:
'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485',
director: ['Xavier Giannoli'],
actors: [
'Benjamin Voisin',
'Cécile de France',
'Vincent Lacoste',
'Xavier Dolan',
'Gérard Depardieu',
'Salomé Dewaels',
'Jeanne Balibar',
'Louis-Do de Lencquesaing',
'Alexis Barbosa',
'Jean-François Stévenin',
'André Marcon',
'Marie Cornillon'
],
writer: ['Xavier Giannoli'],
rating: {
system: 'CSA',
value: '-10'
}
}
])
done()
})
.catch(done)
})
it('can handle empty guide', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const result = await parser({ content })
expect(result).toMatchObject([])
})
const { parser, url } = require('./canalplus.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: 'bi#198',
xmltv_id: 'CanalPlusCinemaFrance.fr'
}
it('can generate valid url for today', done => {
axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
})
} else {
return Promise.resolve({ data: '' })
}
})
const today = dayjs.utc().startOf('d')
url({ channel, date: today })
.then(result => {
expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0'
)
done()
})
.catch(done)
})
it('can generate valid url for tomorrow', done => {
axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
})
} else {
return Promise.resolve({ data: '' })
}
})
const tomorrow = dayjs.utc().startOf('d').add(1, 'd')
url({ channel, date: tomorrow })
.then(result => {
expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1'
)
done()
})
.catch(done)
})
it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => {
if (
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'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
})
} else if (
url ===
'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({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-01-12T06:28:00.000Z',
stop: '2023-01-12T12:06:00.000Z',
title: 'Le cercle',
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.",
image:
'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573',
presenter: ['Lily Bloom'],
rating: {
system: 'CSA',
value: '-10'
}
},
{
start: '2023-01-12T12:06:00.000Z',
stop: '2023-01-12T13:06:00.000Z',
title: 'Illusions perdues',
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...",
image:
'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485',
director: ['Xavier Giannoli'],
actors: [
'Benjamin Voisin',
'Cécile de France',
'Vincent Lacoste',
'Xavier Dolan',
'Gérard Depardieu',
'Salomé Dewaels',
'Jeanne Balibar',
'Louis-Do de Lencquesaing',
'Alexis Barbosa',
'Jean-François Stévenin',
'André Marcon',
'Marie Cornillon'
],
writer: ['Xavier Giannoli'],
rating: {
system: 'CSA',
value: '-10'
}
}
])
done()
})
.catch(done)
})
it('can handle empty guide', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const result = await parser({ content })
expect(result).toMatchObject([])
})

View File

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

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./cgates.lt.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'lrt-televizija-hd',
xmltv_id: 'LRTTV.lt'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(35)
expect(results[0]).toMatchObject({
start: '2022-08-29T21:05:00.000Z',
stop: '2022-08-29T21:30:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016',
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ą.'
})
expect(results[34]).toMatchObject({
start: '2022-08-30T20:45:00.000Z',
stop: '2022-08-30T21:15:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016!'
})
})
it('can handle empty guide', () => {
const result = parser({
content: ''
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./cgates.lt.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'lrt-televizija-hd',
xmltv_id: 'LRTTV.lt'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(35)
expect(results[0]).toMatchObject({
start: '2022-08-29T21:05:00.000Z',
stop: '2022-08-29T21:30:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016',
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ą.'
})
expect(results[34]).toMatchObject({
start: '2022-08-30T20:45:00.000Z',
stop: '2022-08-30T21:15:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016!'
})
})
it('can handle empty guide', () => {
const result = parser({
content: ''
})
expect(result).toMatchObject([])
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,71 +1,71 @@
const { parser, url } = require('./content.astro.com.my.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '425',
xmltv_id: 'TVBClassic.hk'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json')
})
it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => {
if (
url ===
'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json')))
})
} else {
return Promise.resolve({ data: '' })
}
})
let results = await parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(31)
expect(results[0]).toMatchObject({
start: '2022-10-30T16:10:00.000Z',
stop: '2022-10-30T17:02:00.000Z',
title: 'Triumph in the Skies S1 Ep06',
description:
'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'],
directors: ['Joe Ma Tak Chung'],
image: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg',
rating: {
system: 'LPF',
value: 'U'
},
episode: 6,
season: 1,
categories: ['Drama']
})
})
it('can handle empty guide', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
const results = await parser({ date, content })
expect(results).toMatchObject([])
})
const { parser, url } = require('./content.astro.com.my.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '425',
xmltv_id: 'TVBClassic.hk'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json')
})
it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => {
if (
url ===
'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json')))
})
} else {
return Promise.resolve({ data: '' })
}
})
let results = await parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(31)
expect(results[0]).toMatchObject({
start: '2022-10-30T16:10:00.000Z',
stop: '2022-10-30T17:02:00.000Z',
title: 'Triumph in the Skies S1 Ep06',
description:
'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'],
directors: ['Joe Ma Tak Chung'],
image: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg',
rating: {
system: 'LPF',
value: 'U'
},
episode: 6,
season: 1,
categories: ['Drama']
})
})
it('can handle empty guide', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
const results = await parser({ date, content })
expect(results).toMatchObject([])
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,85 +1,85 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'directv.com.uy',
days: 2,
url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming',
request: {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;'
},
data({ channel, date }) {
const [channelNum, channelName] = channel.site_id.split('#')
return {
filterParameters: {
day: date.date(),
time: 0,
minute: 0,
month: date.month() + 1,
year: date.year(),
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum,
channelName: channelName.replace('&amp;', '&')
}
}
}
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
programs.push({
title: item.title,
description: item.description,
rating: parseRating(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseRating(item) {
return item.rating
? {
system: 'MPA',
value: item.rating
}
: null
}
function parseStart(item) {
return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo')
}
function parseStop(item) {
return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo')
}
function parseItems(content, channel) {
if (!content) return []
let [ChannelNumber, ChannelName] = channel.site_id.split('#')
ChannelName = ChannelName.replace('&amp;', '&')
const data = JSON.parse(content)
if (!data || !Array.isArray(data.d)) return []
const channelData = data.d.find(
c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName
)
return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : []
}
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'directv.com.uy',
days: 2,
url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming',
request: {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;'
},
data({ channel, date }) {
const [channelNum, channelName] = channel.site_id.split('#')
return {
filterParameters: {
day: date.date(),
time: 0,
minute: 0,
month: date.month() + 1,
year: date.year(),
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum,
channelName: channelName.replace('&amp;', '&')
}
}
}
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
programs.push({
title: item.title,
description: item.description,
rating: parseRating(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseRating(item) {
return item.rating
? {
system: 'MPA',
value: item.rating
}
: null
}
function parseStart(item) {
return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo')
}
function parseStop(item) {
return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo')
}
function parseItems(content, channel) {
if (!content) return []
let [ChannelNumber, ChannelName] = channel.site_id.split('#')
ChannelName = ChannelName.replace('&amp;', '&')
const data = JSON.parse(content)
if (!data || !Array.isArray(data.d)) return []
const channelData = data.d.find(
c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName
)
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 fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '184#VTV',
xmltv_id: 'VTV.uy'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;'
})
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
filterParameters: {
day: 29,
time: 0,
minute: 0,
month: 8,
year: 2022,
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum: '184',
channelName: 'VTV'
}
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-08-29T03: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',
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).',
rating: {
system: 'MPA',
value: 'NR'
}
})
})
it('can handle empty guide', () => {
const result = parser({
content: '',
channel
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./directv.com.uy.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '184#VTV',
xmltv_id: 'VTV.uy'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;'
})
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
filterParameters: {
day: 29,
time: 0,
minute: 0,
month: 8,
year: 2022,
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum: '184',
channelName: 'VTV'
}
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-08-29T03: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',
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).',
rating: {
system: 'MPA',
value: 'NR'
}
})
})
it('can handle empty guide', () => {
const result = parser({
content: '',
channel
})
expect(result).toMatchObject([])
})

View File

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

View File

@@ -1,96 +1,96 @@
const { parser, url } = require('./directv.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '249#249',
xmltv_id: 'ComedyCentralEast.us'
}
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe(
'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249'
)
})
it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => {
if (url === 'https://www.directv.com/json/program/flip/MV001173520000') {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
})
} else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content, channel })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-01-14T23:00:00.000Z',
stop: '2023-01-15T01:00:00.000Z',
title: 'Men in Black II',
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.',
date: '2002',
image: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg',
category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'],
rating: {
system: 'MPA',
value: 'TV14'
}
},
{
start: '2023-01-15T06:00:00.000Z',
stop: '2023-01-15T06:30:00.000Z',
title: 'South Park',
sub_title: 'Goth Kids 3: Dawn of the Posers',
description: 'The goth kids are sent to a camp for troubled children.',
image:
'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg',
category: ['Series', 'Animation', 'Comedy'],
season: 17,
episode: 4,
rating: {
system: 'MPA',
value: 'TVMA'
}
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))
parser({ content, channel })
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})
const { parser, url } = require('./directv.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '249#249',
xmltv_id: 'ComedyCentralEast.us'
}
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe(
'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249'
)
})
it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => {
if (url === 'https://www.directv.com/json/program/flip/MV001173520000') {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
})
} else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content, channel })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-01-14T23:00:00.000Z',
stop: '2023-01-15T01:00:00.000Z',
title: 'Men in Black II',
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.',
date: '2002',
image: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg',
category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'],
rating: {
system: 'MPA',
value: 'TV14'
}
},
{
start: '2023-01-15T06:00:00.000Z',
stop: '2023-01-15T06:30:00.000Z',
title: 'South Park',
sub_title: 'Goth Kids 3: Dawn of the Posers',
description: 'The goth kids are sent to a camp for troubled children.',
image:
'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg',
category: ['Series', 'Animation', 'Comedy'],
season: 17,
episode: 4,
rating: {
system: 'MPA',
value: 'TVMA'
}
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))
parser({ content, channel })
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})

View File

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

View File

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

View File

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

View File

@@ -1,138 +1,138 @@
const { parser, url } = require('./dna.fi.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-01-15', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ch-216356',
xmltv_id: 'MTV3.fi'
}
it('can generate valid url', async () => {
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'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ date, content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(20)
expect(results[0]).toMatchObject({
start: '2025-01-15T02:30:00.000Z',
stop: '2025-01-15T03:22:00.000Z',
title: {
lang: 'fi',
value: 'Next Level Chef'
},
subtitle: {
lang: 'fi',
value: 'Brunssi'
},
season: 1,
episode: 6,
rating: {
system: 'VET',
value: 'S'
},
date: '2022',
images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/37f/3851073346622580374_aspect_ratio_16_9_1.jpg'
],
description: {
lang: 'fi',
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.'
},
categories: ['Reality TV', 'Entertainment', 'TV Show', 'Next Level Chef', 'Series 1']
})
expect(results[5]).toMatchObject({
title: {
lang: 'fi',
value: 'Kauniit ja rohkeat (S)'
},
subtitle: {
lang: 'fi',
value: 'Parantava syleily'
},
start: '2025-01-15T08:30:00.000Z',
stop: '2025-01-15T09:00:00.000Z',
season: 37,
episode: 9380,
rating: {
system: 'VET',
value: 'S'
},
date: '2023',
images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/79e/6509488401145439178_aspect_ratio_16_9_1.jpg'
],
description: {
lang: 'fi',
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.'
},
categories: [
'Soap',
'Drama',
'Romance',
'Series',
'TV Show',
'The Bold and the Beautiful',
'Series 37'
],
actors: [{ lang: 'en', value: 'Katherine Kelly Lang' }]
})
expect(results[19]).toMatchObject({
start: '2025-01-15T16:30:00.000Z',
stop: '2025-01-15T17:00:00.000Z',
title: {
lang: 'fi',
value: 'Emmerdale (S)'
},
subtitle: {
lang: 'fi',
value: 'Epäilyksen varjossa'
},
season: 54,
episode: 9845,
rating: {
system: 'VET',
value: 'S'
},
date: '2023',
images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/5e8/5978592001161112833_aspect_ratio_16_9_1.jpg'
],
description: {
lang: 'fi',
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.'
},
categories: ['Soap', 'Drama', 'Romance', 'Series', 'TV Show', 'Emmerdale', 'Series 54'],
directors: [
{ lang: 'en', value: 'Ian Bevitt' },
{ lang: 'en', value: 'Munir Malik' }
]
})
})
it('can handle empty guide', () => {
const results = parser({
date,
content: ''
})
expect(results).toMatchObject([])
})
const { parser, url } = require('./dna.fi.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-01-15', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ch-216356',
xmltv_id: 'MTV3.fi'
}
it('can generate valid url', async () => {
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'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ date, content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(20)
expect(results[0]).toMatchObject({
start: '2025-01-15T02:30:00.000Z',
stop: '2025-01-15T03:22:00.000Z',
title: {
lang: 'fi',
value: 'Next Level Chef'
},
subtitle: {
lang: 'fi',
value: 'Brunssi'
},
season: 1,
episode: 6,
rating: {
system: 'VET',
value: 'S'
},
date: '2022',
images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/37f/3851073346622580374_aspect_ratio_16_9_1.jpg'
],
description: {
lang: 'fi',
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.'
},
categories: ['Reality TV', 'Entertainment', 'TV Show', 'Next Level Chef', 'Series 1']
})
expect(results[5]).toMatchObject({
title: {
lang: 'fi',
value: 'Kauniit ja rohkeat (S)'
},
subtitle: {
lang: 'fi',
value: 'Parantava syleily'
},
start: '2025-01-15T08:30:00.000Z',
stop: '2025-01-15T09:00:00.000Z',
season: 37,
episode: 9380,
rating: {
system: 'VET',
value: 'S'
},
date: '2023',
images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/79e/6509488401145439178_aspect_ratio_16_9_1.jpg'
],
description: {
lang: 'fi',
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.'
},
categories: [
'Soap',
'Drama',
'Romance',
'Series',
'TV Show',
'The Bold and the Beautiful',
'Series 37'
],
actors: [{ lang: 'en', value: 'Katherine Kelly Lang' }]
})
expect(results[19]).toMatchObject({
start: '2025-01-15T16:30:00.000Z',
stop: '2025-01-15T17:00:00.000Z',
title: {
lang: 'fi',
value: 'Emmerdale (S)'
},
subtitle: {
lang: 'fi',
value: 'Epäilyksen varjossa'
},
season: 54,
episode: 9845,
rating: {
system: 'VET',
value: 'S'
},
date: '2023',
images: [
'https://mts-pro-cache-vip.dna.fi/meme/v2/5e8/5978592001161112833_aspect_ratio_16_9_1.jpg'
],
description: {
lang: 'fi',
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.'
},
categories: ['Soap', 'Drama', 'Romance', 'Series', 'TV Show', 'Emmerdale', 'Series 54'],
directors: [
{ lang: 'en', value: 'Ian Bevitt' },
{ lang: 'en', value: 'Munir Malik' }
]
})
})
it('can handle empty guide', () => {
const results = parser({
date,
content: ''
})
expect(results).toMatchObject([])
})

View File

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

View File

@@ -1,82 +1,82 @@
const { parser, url } = require('./dsmart.com.tr.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const fs = require('fs')
const path = require('path')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '5fe07f5dcfef0b1593275822',
xmltv_id: 'Sinema1001.tr'
}
axios.get.mockImplementation(url => {
const result = {}
const urls = {
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13':
'content1.json',
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=2&limit=10&day=2025-01-13':
'content2.json',
}
if (urls[url] !== undefined) {
result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString()
if (!urls[url].startsWith('content1')) {
result.data = JSON.parse(result.data)
}
}
return Promise.resolve(result)
})
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13'
)
})
it('can parse response', async () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')).toString()
const results = (await parser({ content, channel, date })).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(11)
expect(results[0]).toMatchObject({
start: '2025-01-12T21:30:00.000Z',
stop: '2025-01-12T23:30:00.000Z',
title: 'Taksi Şoförü',
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.',
category: ['Sinema', 'Genel']
})
expect(results[10]).toMatchObject({
start: '2025-01-13T19:00:00.000Z',
stop: '2025-01-13T21:00:00.000Z',
title: 'Senin Adın',
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.',
category: ['Sinema', 'Genel']
})
})
it('can handle empty guide', async () => {
const results = await parser({
channel,
date,
content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')).toString(),
useCache: false
})
expect(results).toMatchObject([])
})
const { parser, url } = require('./dsmart.com.tr.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const fs = require('fs')
const path = require('path')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '5fe07f5dcfef0b1593275822',
xmltv_id: 'Sinema1001.tr'
}
axios.get.mockImplementation(url => {
const result = {}
const urls = {
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13':
'content1.json',
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=2&limit=10&day=2025-01-13':
'content2.json',
}
if (urls[url] !== undefined) {
result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString()
if (!urls[url].startsWith('content1')) {
result.data = JSON.parse(result.data)
}
}
return Promise.resolve(result)
})
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=1&limit=10&day=2025-01-13'
)
})
it('can parse response', async () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')).toString()
const results = (await parser({ content, channel, date })).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(11)
expect(results[0]).toMatchObject({
start: '2025-01-12T21:30:00.000Z',
stop: '2025-01-12T23:30:00.000Z',
title: 'Taksi Şoförü',
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.',
category: ['Sinema', 'Genel']
})
expect(results[10]).toMatchObject({
start: '2025-01-13T19:00:00.000Z',
stop: '2025-01-13T21:00:00.000Z',
title: 'Senin Adın',
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.',
category: ['Sinema', 'Genel']
})
})
it('can handle empty guide', async () => {
const results = await parser({
channel,
date,
content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json')).toString(),
useCache: false
})
expect(results).toMatchObject([])
})

View File

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

View File

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

View File

@@ -1,79 +1,79 @@
const { parser, url } = require('./dtv8.net.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-02-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => {
expect(url({ date })).toBe('https://dtv8.net/tv-listings/friday/')
})
it('can parse response for friday', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_fri.html'))
let results = parser({ content, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(18)
expect(results[9]).toMatchObject({
title: 'Smallville',
image: 'http://dtv8.net/wp-content/uploads/71P0aShCBXL._SL1300_.jpg',
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.',
start: '2025-02-21T21:00:00.000Z',
stop: '2025-02-21T22:00:00.000Z'
})
expect(results[15]).toMatchObject({
title: 'Law & Order',
image: null,
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.',
start: '2025-02-22T01:45:00.000Z',
stop: '2025-02-22T02:30:00.000Z'
})
})
it('can parse response for saturday', () => {
const date = dayjs.utc('2025-02-22', 'YYYY-MM-DD').startOf('d')
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_sat.html'))
let results = parser({ content, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(11)
expect(results[0]).toMatchObject({
title: 'Sign On',
image: null,
description: null,
start: '2025-02-22T13:55:00.000Z',
stop: '2025-02-22T14:00:00.000Z'
})
expect(results[10]).toMatchObject({
title: 'Sign Off',
image: null,
description: null,
start: '2025-02-23T04:00:00.000Z',
stop: '2025-02-23T04:30:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
const { parser, url } = require('./dtv8.net.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-02-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => {
expect(url({ date })).toBe('https://dtv8.net/tv-listings/friday/')
})
it('can parse response for friday', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_fri.html'))
let results = parser({ content, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(18)
expect(results[9]).toMatchObject({
title: 'Smallville',
image: 'http://dtv8.net/wp-content/uploads/71P0aShCBXL._SL1300_.jpg',
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.',
start: '2025-02-21T21:00:00.000Z',
stop: '2025-02-21T22:00:00.000Z'
})
expect(results[15]).toMatchObject({
title: 'Law & Order',
image: null,
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.',
start: '2025-02-22T01:45:00.000Z',
stop: '2025-02-22T02:30:00.000Z'
})
})
it('can parse response for saturday', () => {
const date = dayjs.utc('2025-02-22', 'YYYY-MM-DD').startOf('d')
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_sat.html'))
let results = parser({ content, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(11)
expect(results[0]).toMatchObject({
title: 'Sign On',
image: null,
description: null,
start: '2025-02-22T13:55:00.000Z',
stop: '2025-02-22T14:00:00.000Z'
})
expect(results[10]).toMatchObject({
title: 'Sign Off',
image: null,
description: null,
start: '2025-02-23T04:00:00.000Z',
stop: '2025-02-23T04:30:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,33 @@
const parser = require('epg-parser')
module.exports = {
site: 'energeek.cl',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml',
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
icon: item.icon?.[0]?.src,
start: item.start,
stop: item.stop
})
})
return programs
}
}
function parseItems(content, channel, date) {
const { programs } = parser.parse(content)
return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day'))
}
const parser = require('epg-parser')
module.exports = {
site: 'energeek.cl',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml',
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
icon: item.icon?.[0]?.src,
start: item.start,
stop: item.stop
})
})
return programs
}
}
function parseItems(content, channel, date) {
const { programs } = parser.parse(content)
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 fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'EnerGeek Retro',
xmltv_id: 'EnerGeekRetro.cl'
}
it('can generate valid url', () => {
expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
let results = parser({ content, channel, date })
expect(results[0]).toMatchObject({
start: '2022-11-29T03:00:00.000Z',
stop: '2022-11-29T03:30:00.000Z',
title: 'Noir',
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',
icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg'
})
})
it('can handle empty guide', () => {
const result = parser({ content: '', channel, date })
expect(result).toMatchObject([])
})
const { parser, url } = require('./energeek.cl.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'EnerGeek Retro',
xmltv_id: 'EnerGeekRetro.cl'
}
it('can generate valid url', () => {
expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
let results = parser({ content, channel, date })
expect(results[0]).toMatchObject({
start: '2022-11-29T03:00:00.000Z',
stop: '2022-11-29T03:30:00.000Z',
title: 'Noir',
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',
icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg'
})
})
it('can handle empty guide', () => {
const result = parser({ content: '', channel, date })
expect(result).toMatchObject([])
})

View File

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

View File

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

View File

@@ -1,45 +1,45 @@
const axios = require('axios')
const parser = require('epg-parser')
module.exports = {
site: 'epg.112114.xyz',
days: 1,
url: 'https://epg.112114.xyz/pp.xml',
request: {
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
start: item.start,
stop: item.stop
})
})
return programs
},
async channels() {
const data = await axios
.get('https://epg.112114.xyz/pp.xml')
.then(r => r.data)
.catch(console.log)
const { channels } = parser.parse(data)
return channels.map(channel => ({
lang: 'zh',
site_id: channel.id,
name: channel.displayName[0].value
}))
}
}
function parseItems(content, channel, date) {
const { programs } = parser.parse(content)
return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day'))
}
const axios = require('axios')
const parser = require('epg-parser')
module.exports = {
site: 'epg.112114.xyz',
days: 1,
url: 'https://epg.112114.xyz/pp.xml',
request: {
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
start: item.start,
stop: item.stop
})
})
return programs
},
async channels() {
const data = await axios
.get('https://epg.112114.xyz/pp.xml')
.then(r => r.data)
.catch(console.log)
const { channels } = parser.parse(data)
return channels.map(channel => ({
lang: 'zh',
site_id: channel.id,
name: channel.displayName[0].value
}))
}
}
function parseItems(content, channel, date) {
const { programs } = parser.parse(content)
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 dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const fs = require('fs')
const path = require('path')
dayjs.extend(utc)
dayjs.extend(timezone)
const date = dayjs.utc('2025-01-11', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'BTV文艺', xmltv_id: 'BRTVArtsChannel.cn', lang: 'zh' }
it('can generate valid url', () => {
expect(url).toBe('https://epg.112114.xyz/pp.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
const results = parser({ date, content, channel })
expect(results.length).toBe(28)
expect(results[0]).toMatchObject({
start: '2025-01-11T00:07:00.000Z',
stop: '2025-01-11T00:24:00.000Z',
title: '每日文艺播报'
})
expect(results[27]).toMatchObject({
start: '2025-01-11T15:16:00.000Z',
stop: '2025-01-11T15:59:00.000Z',
title: '笑动剧场'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: ''
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./epg.112114.xyz.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const fs = require('fs')
const path = require('path')
dayjs.extend(utc)
dayjs.extend(timezone)
const date = dayjs.utc('2025-01-11', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'BTV文艺', xmltv_id: 'BRTVArtsChannel.cn', lang: 'zh' }
it('can generate valid url', () => {
expect(url).toBe('https://epg.112114.xyz/pp.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'))
const results = parser({ date, content, channel })
expect(results.length).toBe(28)
expect(results[0]).toMatchObject({
start: '2025-01-11T00:07:00.000Z',
stop: '2025-01-11T00:24:00.000Z',
title: '每日文艺播报'
})
expect(results[27]).toMatchObject({
start: '2025-01-11T15:16:00.000Z',
stop: '2025-01-11T15:59:00.000Z',
title: '笑动剧场'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: ''
})
expect(result).toMatchObject([])
})

View File

@@ -1,64 +1,64 @@
const axios = require('axios')
const iconv = require('iconv-lite')
const parser = require('epg-parser')
const { ungzip } = require('pako')
let cachedContent
module.exports = {
site: 'epg.iptvx.one',
days: 2,
url: 'https://iptvx.one/epg/epg_noarch.xml.gz',
request: {
maxContentLength: 500000000, // 500 MB
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
parser: function ({ buffer, channel, date, cached }) {
if (!cached) cachedContent = undefined
let programs = []
const items = parseItems(buffer, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
start: item.start,
stop: item.stop
})
})
return programs
},
async channels() {
const data = await axios
.get('https://epg.iptvx.one/api/channels.json')
.then(r => r.data)
.catch(console.log)
return data.channels.map(channel => {
const [name] = channel.chan_names.split(' • ')
return {
lang: 'ru',
site_id: channel.chan_id,
name
}
})
}
}
function parseItems(buffer, channel, date) {
if (!buffer) return []
if (!cachedContent) {
const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8')
cachedContent = parser.parse(encoded)
}
const { programs } = cachedContent
return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day'))
}
const axios = require('axios')
const iconv = require('iconv-lite')
const parser = require('epg-parser')
const { ungzip } = require('pako')
let cachedContent
module.exports = {
site: 'epg.iptvx.one',
days: 2,
url: 'https://iptvx.one/epg/epg_noarch.xml.gz',
request: {
maxContentLength: 500000000, // 500 MB
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
parser: function ({ buffer, channel, date, cached }) {
if (!cached) cachedContent = undefined
let programs = []
const items = parseItems(buffer, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
start: item.start,
stop: item.stop
})
})
return programs
},
async channels() {
const data = await axios
.get('https://epg.iptvx.one/api/channels.json')
.then(r => r.data)
.catch(console.log)
return data.channels.map(channel => {
const [name] = channel.chan_names.split(' • ')
return {
lang: 'ru',
site_id: channel.chan_id,
name
}
})
}
}
function parseItems(buffer, channel, date) {
if (!buffer) return []
if (!cachedContent) {
const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8')
cachedContent = parser.parse(encoded)
}
const { programs } = cachedContent
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 dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const fs = require('fs')
const path = require('path')
dayjs.extend(utc)
dayjs.extend(timezone)
const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '12-omsk', xmltv_id: 'Channel12.ru' }
it('can generate valid url', () => {
expect(url).toBe('https://iptvx.one/epg/epg_noarch.xml.gz')
})
it('can parse response', () => {
const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz'))
const results = parser({ date, buffer, channel })
expect(results.length).toBe(29)
expect(results[0]).toMatchObject({
start: '2025-01-13T00:00:00.000Z',
stop: '2025-01-13T00:55:00.000Z',
title: 'Акценты недели',
description:
'Программа расскажет зрителям о том, как развивались самые яркие события недели, поможет расставить акценты над самыми обсуждаемыми новостями. Россия, ток-шоу'
})
expect(results[28]).toMatchObject({
start: '2025-01-13T22:15:00.000Z',
stop: '2025-01-14T00:00:00.000Z',
title: 'д/с Необыкновенные люди',
description:
'Герои цикла врачи, спортсмены, представители творческих профессий, волонтеры и многие-многие другие. Их деятельность связана с жизнью особенных людей. Россия, док. сериал'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
buffer: ''
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./epg.iptvx.one.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const fs = require('fs')
const path = require('path')
dayjs.extend(utc)
dayjs.extend(timezone)
const date = dayjs.utc('2025-01-13', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '12-omsk', xmltv_id: 'Channel12.ru' }
it('can generate valid url', () => {
expect(url).toBe('https://iptvx.one/epg/epg_noarch.xml.gz')
})
it('can parse response', () => {
const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz'))
const results = parser({ date, buffer, channel })
expect(results.length).toBe(29)
expect(results[0]).toMatchObject({
start: '2025-01-13T00:00:00.000Z',
stop: '2025-01-13T00:55:00.000Z',
title: 'Акценты недели',
description:
'Программа расскажет зрителям о том, как развивались самые яркие события недели, поможет расставить акценты над самыми обсуждаемыми новостями. Россия, ток-шоу'
})
expect(results[28]).toMatchObject({
start: '2025-01-13T22:15:00.000Z',
stop: '2025-01-14T00:00:00.000Z',
title: 'д/с Необыкновенные люди',
description:
'Герои цикла врачи, спортсмены, представители творческих профессий, волонтеры и многие-многие другие. Их деятельность связана с жизнью особенных людей. Россия, док. сериал'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
buffer: ''
})
expect(result).toMatchObject([])
})

View File

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

View File

@@ -1,94 +1,94 @@
const { parser, url, request } = require('./epg.telemach.ba.config.js')
const fs = require('fs')
const axios = require('axios')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
axios.post.mockImplementation((url, data, opts) => {
if (
url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' &&
JSON.stringify(opts.headers) ===
JSON.stringify({
Authorization:
'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
})
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
})
} else {
return Promise.resolve({
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 channel = {
site_id: '1607',
xmltv_id: 'N1HD.hr'
}
it('can generate valid url', async () => {
const result = url({ date, channel })
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'
)
})
it('can generate valid request headers', async () => {
const result = await request.headers()
expect(result).toMatchObject({
Authorization:
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(35)
expect(results[0]).toMatchObject({
start: '2025-01-20T00:00:00.000Z',
stop: '2025-01-20T00:30:00.000Z',
title: 'DW Euromaxx',
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.',
image:
'https://images-web.ug-be.cdn.united.cloud/2021/02/18/06/05/21/stb_xl_cd4f72e01d308ecce782e29b69af7de6707b9e85.jpg',
season: null,
episode: null
})
expect(results[34]).toMatchObject({
start: '2025-01-20T23:50:00.000Z',
stop: '2025-01-21T00:00:00.000Z',
title: 'DW Shift',
description: 'Tjedni magazin koji nam donosi najnovije vijesti vezane za Internet.',
image:
'https://images-web.ug-be.cdn.united.cloud/2023/06/09/13/07/53/stb_xl_0849d5d70c1337651b85b6335e340e15bd5d6a73_340fc454bc73019d052cf936ebee5da3.jpg',
season: null,
episode: null
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
})
expect(results).toMatchObject([])
})
const { parser, url, request } = require('./epg.telemach.ba.config.js')
const fs = require('fs')
const axios = require('axios')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
axios.post.mockImplementation((url, data, opts) => {
if (
url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' &&
JSON.stringify(opts.headers) ===
JSON.stringify({
Authorization:
'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
})
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
})
} else {
return Promise.resolve({
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 channel = {
site_id: '1607',
xmltv_id: 'N1HD.hr'
}
it('can generate valid url', async () => {
const result = url({ date, channel })
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'
)
})
it('can generate valid request headers', async () => {
const result = await request.headers()
expect(result).toMatchObject({
Authorization:
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(35)
expect(results[0]).toMatchObject({
start: '2025-01-20T00:00:00.000Z',
stop: '2025-01-20T00:30:00.000Z',
title: 'DW Euromaxx',
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.',
image:
'https://images-web.ug-be.cdn.united.cloud/2021/02/18/06/05/21/stb_xl_cd4f72e01d308ecce782e29b69af7de6707b9e85.jpg',
season: null,
episode: null
})
expect(results[34]).toMatchObject({
start: '2025-01-20T23:50:00.000Z',
stop: '2025-01-21T00:00:00.000Z',
title: 'DW Shift',
description: 'Tjedni magazin koji nam donosi najnovije vijesti vezane za Internet.',
image:
'https://images-web.ug-be.cdn.united.cloud/2023/06/09/13/07/53/stb_xl_0849d5d70c1337651b85b6335e340e15bd5d6a73_340fc454bc73019d052cf936ebee5da3.jpg',
season: null,
episode: null
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
})
expect(results).toMatchObject([])
})

View File

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

View File

@@ -1,96 +1,96 @@
const { parser, url, request } = require('./epg.telemach.me.config.js')
const fs = require('fs')
const axios = require('axios')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
axios.post.mockImplementation((url, data, opts) => {
if (
url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' &&
JSON.stringify(opts.headers) ===
JSON.stringify({
Authorization:
'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
})
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
})
} else {
return Promise.resolve({
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 channel = {
site_id: '92',
xmltv_id: 'PinkKids.rs'
}
it('can generate valid url', async () => {
const result = url({ date, channel })
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'
)
})
it('can generate valid request headers', async () => {
const result = await request.headers()
expect(result).toMatchObject({
Authorization:
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg',
Referer: 'https://epg.telemach.me/'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(55)
expect(results[0]).toMatchObject({
start: '2025-01-19T23:20:00.000Z',
stop: '2025-01-20T00:10:00.000Z',
title: 'Pinkove Zvezdice',
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...',
image:
'https://images-web.ug-be.cdn.united.cloud/2023/06/22/11/19/19/stb_xl_115752ec1e05872b86ceda7726d347f533e17f43_340fc454bc73019d052cf936ebee5da3.jpg',
season: null,
episode: null
})
expect(results[54]).toMatchObject({
start: '2025-01-20T23:50:00.000Z',
stop: '2025-01-21T00:10:00.000Z',
title: 'Hajdi',
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...',
image:
'https://images-web.ug-be.cdn.united.cloud/2024/05/10/14/49/09/stb_xl_7d1c73ee4df7de5c4157e9daccae098d50ee853d_99230e7f5bdc95451f37aa31f8425b4d.jpg',
season: null,
episode: null
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
})
expect(results).toMatchObject([])
})
const { parser, url, request } = require('./epg.telemach.me.config.js')
const fs = require('fs')
const axios = require('axios')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
axios.post.mockImplementation((url, data, opts) => {
if (
url === 'https://api-web.ug-be.cdn.united.cloud/oauth/token?grant_type=client_credentials' &&
JSON.stringify(opts.headers) ===
JSON.stringify({
Authorization:
'Basic MjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1OjEyejJzMXJ3bXdhZmsxMGNkdzl0cjloOWFjYjZwdjJoZDhscXZ0aGc='
})
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json')))
})
} else {
return Promise.resolve({
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 channel = {
site_id: '92',
xmltv_id: 'PinkKids.rs'
}
it('can generate valid url', async () => {
const result = url({ date, channel })
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'
)
})
it('can generate valid request headers', async () => {
const result = await request.headers()
expect(result).toMatchObject({
Authorization:
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidWMtaW5mby1zZXJ2aWNlIl0sInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNzM3Mzc3NDUxLCJhdXRob3JpdGllcyI6WyJST0xFX1BVQkxJQ19FUEciXSwianRpIjoiUVBubHdRSDczS1EwSnU0WDZwRTc2Zm5mUmRnIiwiY2xpZW50X2lkIjoiMjdlMTFmNWUtODhlMi00OGU0LWJkNDItOGUxNWFiYmM2NmY1In0.LqJAZUWEqIOcLrRSMpxZxnF-f1arKbHgfweLMXt-MBjCDbVJD39OQEsh_b68mtePAoa3n8LRbf3IFT40Ys5Vbe-k_Btm4a9gdEGr6cNi_4HGk4Bto6RUDvCp59VRfoRZhWe145Q2b5TS6szmC4Ws2YWIcZU5vrJcYs2GZiCk6U11MOcd1i52WmZj8cLPq0ZPDB_bzmTgYkvkVa7zOzUOPSl4M8T6fPUa__vVKUt0jOgtFoHeue2mQVgISC2puEGsBN0jJwvJ8PzM6IVxXrQno3MBv0VJy_qILiFPcxRePGRAmKLuEqagvikO7P_XQgFjZgg-j8u8wX2WwO0Yxft0Pg',
Referer: 'https://epg.telemach.me/'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
let results = parser({ content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(55)
expect(results[0]).toMatchObject({
start: '2025-01-19T23:20:00.000Z',
stop: '2025-01-20T00:10:00.000Z',
title: 'Pinkove Zvezdice',
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...',
image:
'https://images-web.ug-be.cdn.united.cloud/2023/06/22/11/19/19/stb_xl_115752ec1e05872b86ceda7726d347f533e17f43_340fc454bc73019d052cf936ebee5da3.jpg',
season: null,
episode: null
})
expect(results[54]).toMatchObject({
start: '2025-01-20T23:50:00.000Z',
stop: '2025-01-21T00:10:00.000Z',
title: 'Hajdi',
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...',
image:
'https://images-web.ug-be.cdn.united.cloud/2024/05/10/14/49/09/stb_xl_7d1c73ee4df7de5c4157e9daccae098d50ee853d_99230e7f5bdc95451f37aa31f8425b4d.jpg',
season: null,
episode: null
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
})
expect(results).toMatchObject([])
})

View File

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

View File

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

View File

@@ -1,75 +1,75 @@
const axios = require('axios')
const iconv = require('iconv-lite')
const parser = require('epg-parser')
const { ungzip } = require('pako')
let cachedContent
module.exports = {
site: 'epgshare01.online',
days: 2,
url({ channel }) {
const [tag] = channel.site_id.split('#')
return `https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`
},
request: {
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
},
maxContentLength: 100000000 // 100 MB
},
parser({ buffer, channel, date, cached }) {
if (!cached) cachedContent = undefined
let programs = []
const items = parseItems(buffer, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
start: item.start,
stop: item.stop
})
})
return programs
},
async channels({ tag }) {
const buffer = await axios
.get(`https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`, {
responseType: 'arraybuffer'
})
.then(r => r.data)
.catch(console.error)
const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8')
const { channels } = parser.parse(encoded)
return channels.map(channel => {
const displayName = channel.displayName[0]
return {
lang: displayName.lang || 'en',
site_id: `${tag}#${channel.id}`,
name: displayName.value
}
})
}
}
function parseItems(buffer, channel, date) {
if (!buffer) return []
if (!cachedContent) {
const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8')
cachedContent = parser.parse(encoded)
}
const { programs } = cachedContent
const [, channelId] = channel.site_id.split('#')
return programs.filter(p => p.channel === channelId && date.isSame(p.start, 'day'))
}
const axios = require('axios')
const iconv = require('iconv-lite')
const parser = require('epg-parser')
const { ungzip } = require('pako')
let cachedContent
module.exports = {
site: 'epgshare01.online',
days: 2,
url({ channel }) {
const [tag] = channel.site_id.split('#')
return `https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`
},
request: {
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
},
maxContentLength: 100000000 // 100 MB
},
parser({ buffer, channel, date, cached }) {
if (!cached) cachedContent = undefined
let programs = []
const items = parseItems(buffer, channel, date)
items.forEach(item => {
programs.push({
title: item.title?.[0]?.value,
description: item.desc?.[0]?.value,
start: item.start,
stop: item.stop
})
})
return programs
},
async channels({ tag }) {
const buffer = await axios
.get(`https://epgshare01.online/epgshare01/epg_ripper_${tag}.xml.gz`, {
responseType: 'arraybuffer'
})
.then(r => r.data)
.catch(console.error)
const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8')
const { channels } = parser.parse(encoded)
return channels.map(channel => {
const displayName = channel.displayName[0]
return {
lang: displayName.lang || 'en',
site_id: `${tag}#${channel.id}`,
name: displayName.value
}
})
}
}
function parseItems(buffer, channel, date) {
if (!buffer) return []
if (!cachedContent) {
const content = ungzip(buffer)
const encoded = iconv.decode(content, 'utf8')
cachedContent = parser.parse(encoded)
}
const { programs } = cachedContent
const [, channelId] = channel.site_id.split('#')
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 fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-02-09', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'ALJAZEERA1#AlJazeera.English.net' }
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://epgshare01.online/epgshare01/epg_ripper_ALJAZEERA1.xml.gz')
})
it('can parse response', () => {
const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz'))
const results = parser({ buffer, channel, date, cached: false })
expect(results.length).toBe(40)
expect(results[0]).toMatchObject({
title: 'The Palestine Laboratory',
description:
"Exposing how Israel's sales of military technology is aiding state control around the world.",
start: '2025-02-09T00:00:00.000Z',
stop: '2025-02-09T01:00:00.000Z'
})
expect(results[39]).toMatchObject({
title: 'Inside Story',
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.',
start: '2025-02-09T23:30:00.000Z',
stop: '2025-02-10T00:00:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({ content: '', channel, date, cached: false })
expect(results).toMatchObject([])
})
const { parser, url } = require('./epgshare01.online.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-02-09', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'ALJAZEERA1#AlJazeera.English.net' }
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://epgshare01.online/epgshare01/epg_ripper_ALJAZEERA1.xml.gz')
})
it('can parse response', () => {
const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml.gz'))
const results = parser({ buffer, channel, date, cached: false })
expect(results.length).toBe(40)
expect(results[0]).toMatchObject({
title: 'The Palestine Laboratory',
description:
"Exposing how Israel's sales of military technology is aiding state control around the world.",
start: '2025-02-09T00:00:00.000Z',
stop: '2025-02-09T01:00:00.000Z'
})
expect(results[39]).toMatchObject({
title: 'Inside Story',
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.',
start: '2025-02-09T23:30:00.000Z',
stop: '2025-02-10T00:00:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({ content: '', channel, date, cached: false })
expect(results).toMatchObject([])
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,52 +1,52 @@
const dayjs = require('dayjs')
module.exports = {
site: 'frikanalen.no',
days: 2,
url({ date }) {
return `https://frikanalen.no/api/scheduleitems/?date=${date.format(
'YYYY-MM-DD'
)}&format=json&limit=100`
},
parser({ content }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: parseTitle(item),
category: parseCategory(item),
description: parseDescription(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseTitle(item) {
return item.video.name
}
function parseCategory(item) {
return item.video.categories
}
function parseDescription(item) {
return item.video.header
}
function parseStart(item) {
return dayjs(item.starttime)
}
function parseStop(item) {
return dayjs(item.endtime)
}
function parseItems(content) {
const data = JSON.parse(content)
return data && Array.isArray(data.results) ? data.results : []
}
const dayjs = require('dayjs')
module.exports = {
site: 'frikanalen.no',
days: 2,
url({ date }) {
return `https://frikanalen.no/api/scheduleitems/?date=${date.format(
'YYYY-MM-DD'
)}&format=json&limit=100`
},
parser({ content }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: parseTitle(item),
category: parseCategory(item),
description: parseDescription(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseTitle(item) {
return item.video.name
}
function parseCategory(item) {
return item.video.categories
}
function parseDescription(item) {
return item.video.header
}
function parseStart(item) {
return dayjs(item.starttime)
}
function parseStop(item) {
return dayjs(item.endtime)
}
function parseItems(content) {
const data = JSON.parse(content)
return data && Array.isArray(data.results) ? data.results : []
}

View File

@@ -1,64 +1,64 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'galamtv.kz',
timezone: 'Asia/Almaty',
days: 2,
request: {
method: 'GET',
headers: {
Referer: 'https://galamtv.kz/',
Origin: 'https://galamtv.kz',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br, zstd'
}
},
url({ channel, date }) {
const todayEpoch = date.startOf('day').unix()
const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
return `https://galam.server-api.lfstrm.tv/channels/${channel.site_id}/programs?period=${todayEpoch}:${nextDayEpoch}`
},
parser: function ({ content }) {
let programs = []
const data = JSON.parse(content)
const programsData = data.programs || []
programsData.forEach(program => {
const start = dayjs.unix(program.scheduleInfo.start)
const stop = dayjs.unix(program.scheduleInfo.end)
programs.push({
title: program.metaInfo.title,
description: program.metaInfo.description,
image: program.mediaInfo.thumbnails[0].url,
start,
stop
})
})
return programs
},
async channels() {
try {
const response = await axios.get('https://galam.server-api.lfstrm.tv/channels-now')
return response.data.channels.map(item => {
return {
lang: 'kk',
site_id: item.channels.id,
name: item.channels.info.metaInfo.title
}
})
} catch (error) {
console.error('Error fetching channels:', error)
return []
}
}
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'galamtv.kz',
timezone: 'Asia/Almaty',
days: 2,
request: {
method: 'GET',
headers: {
Referer: 'https://galamtv.kz/',
Origin: 'https://galamtv.kz',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br, zstd'
}
},
url({ channel, date }) {
const todayEpoch = date.startOf('day').unix()
const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
return `https://galam.server-api.lfstrm.tv/channels/${channel.site_id}/programs?period=${todayEpoch}:${nextDayEpoch}`
},
parser: function ({ content }) {
let programs = []
const data = JSON.parse(content)
const programsData = data.programs || []
programsData.forEach(program => {
const start = dayjs.unix(program.scheduleInfo.start)
const stop = dayjs.unix(program.scheduleInfo.end)
programs.push({
title: program.metaInfo.title,
description: program.metaInfo.description,
image: program.mediaInfo.thumbnails[0].url,
start,
stop
})
})
return programs
},
async channels() {
try {
const response = await axios.get('https://galam.server-api.lfstrm.tv/channels-now')
return response.data.channels.map(item => {
return {
lang: 'kk',
site_id: item.channels.id,
name: item.channels.info.metaInfo.title
}
})
} catch (error) {
console.error('Error fetching channels:', error)
return []
}
}
}

View File

@@ -1,102 +1,102 @@
const axios = require('axios')
const cheerio = require('cheerio')
const url = require('url')
const path = require('path')
const { DateTime } = require('luxon')
module.exports = {
site: 'gatotv.com',
days: 2,
url({ channel, date }) {
return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}`
},
parser({ content, date }) {
let programs = []
const items = parseItems(content)
date = date.subtract(1, 'd')
items.forEach((item, i) => {
const $item = cheerio.load(item)
let start = parseStart($item, date)
if (i === 0 && start.hour >= 5) {
start = start.plus({ days: 1 })
date = date.add(1, 'd')
}
let stop = parseStop($item, date)
if (stop < start) {
stop = stop.plus({ days: 1 })
date = date.add(1, 'd')
}
programs.push({
title: parseTitle($item),
description: parseDescription($item),
image: parseImage($item),
start,
stop
})
})
return programs
},
async channels() {
const data = await axios
.get('https://www.gatotv.com/guia_tv/completa')
.then(response => response.data)
.catch(console.log)
const $ = cheerio.load(data)
const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray()
return items.map(item => {
const $item = cheerio.load(item)
const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href')
const parsed = url.parse(link)
return {
lang: 'es',
site_id: path.basename(parsed.pathname),
name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text()
}
})
}
}
function parseTitle($item) {
return $item(
'td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span'
).text()
}
function parseDescription($item) {
return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim()
}
function parseImage($item) {
return $item('td:nth-child(3) > a > img').attr('src')
}
function parseStart($item, date) {
const time = $item('td:nth-child(1) > div > time').attr('datetime')
return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'EST'
}).toUTC()
}
function parseStop($item, date) {
const time = $item('td:nth-child(2) > div > time').attr('datetime')
return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'EST'
}).toUTC()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $(
'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG'
)
.find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected')
.toArray()
}
const axios = require('axios')
const cheerio = require('cheerio')
const url = require('url')
const path = require('path')
const { DateTime } = require('luxon')
module.exports = {
site: 'gatotv.com',
days: 2,
url({ channel, date }) {
return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}`
},
parser({ content, date }) {
let programs = []
const items = parseItems(content)
date = date.subtract(1, 'd')
items.forEach((item, i) => {
const $item = cheerio.load(item)
let start = parseStart($item, date)
if (i === 0 && start.hour >= 5) {
start = start.plus({ days: 1 })
date = date.add(1, 'd')
}
let stop = parseStop($item, date)
if (stop < start) {
stop = stop.plus({ days: 1 })
date = date.add(1, 'd')
}
programs.push({
title: parseTitle($item),
description: parseDescription($item),
image: parseImage($item),
start,
stop
})
})
return programs
},
async channels() {
const data = await axios
.get('https://www.gatotv.com/guia_tv/completa')
.then(response => response.data)
.catch(console.log)
const $ = cheerio.load(data)
const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray()
return items.map(item => {
const $item = cheerio.load(item)
const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href')
const parsed = url.parse(link)
return {
lang: 'es',
site_id: path.basename(parsed.pathname),
name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text()
}
})
}
}
function parseTitle($item) {
return $item(
'td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span'
).text()
}
function parseDescription($item) {
return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim()
}
function parseImage($item) {
return $item('td:nth-child(3) > a > img').attr('src')
}
function parseStart($item, date) {
const time = $item('td:nth-child(1) > div > time').attr('datetime')
return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'EST'
}).toUTC()
}
function parseStop($item, date) {
const time = $item('td:nth-child(2) > div > time').attr('datetime')
return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'EST'
}).toUTC()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $(
'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG'
)
.find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected')
.toArray()
}

View File

@@ -1,79 +1,79 @@
const { parser, url } = require('./gatotv.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'm_0',
xmltv_id: '0porMovistarPlus.es'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8')
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-13T04:30:00.000Z',
stop: '2023-06-13T05:32:00.000Z',
title: 'Supergarcía'
})
expect(results[1]).toMatchObject({
start: '2023-06-13T05:32:00.000Z',
stop: '2023-06-13T06:59:00.000Z',
title: 'La resistencia'
})
expect(results[25]).toMatchObject({
start: '2023-06-14T04:46:00.000Z',
stop: '2023-06-14T05:00:00.000Z',
title: 'Una familia absolutamente normal'
})
})
it('can parse response when the guide starts from midnight', () => {
date = date.add(1, 'd')
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8')
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-14T05:00:00.000Z',
stop: '2023-06-14T05:32:00.000Z',
title: 'Ilustres Ignorantes'
})
expect(results[26]).toMatchObject({
start: '2023-06-15T04:30:00.000Z',
stop: '2023-06-15T05:30:00.000Z',
title: 'Showriano'
})
})
it('can handle empty guide', () => {
const results = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(results).toMatchObject([])
})
const { parser, url } = require('./gatotv.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'm_0',
xmltv_id: '0porMovistarPlus.es'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8')
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-13T04:30:00.000Z',
stop: '2023-06-13T05:32:00.000Z',
title: 'Supergarcía'
})
expect(results[1]).toMatchObject({
start: '2023-06-13T05:32:00.000Z',
stop: '2023-06-13T06:59:00.000Z',
title: 'La resistencia'
})
expect(results[25]).toMatchObject({
start: '2023-06-14T04:46:00.000Z',
stop: '2023-06-14T05:00:00.000Z',
title: 'Una familia absolutamente normal'
})
})
it('can parse response when the guide starts from midnight', () => {
date = date.add(1, 'd')
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8')
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-14T05:00:00.000Z',
stop: '2023-06-14T05:32:00.000Z',
title: 'Ilustres Ignorantes'
})
expect(results[26]).toMatchObject({
start: '2023-06-15T04:30:00.000Z',
stop: '2023-06-15T05:30:00.000Z',
title: 'Showriano'
})
})
it('can handle empty guide', () => {
const results = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(results).toMatchObject([])
})

View File

@@ -1,64 +1,64 @@
const table2array = require('table2array')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const isoWeek = require('dayjs/plugin/isoWeek')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
dayjs.extend(isoWeek)
module.exports = {
site: 'getafteritmedia.com',
days: 2,
url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml',
parser({ content, channel, date }) {
const programs = []
const items = parseItems(content, channel, date)
items.forEach(item => {
const prev = programs[programs.length - 1]
let start = parseStart(item, date)
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: item.title,
start,
stop
})
})
return programs
}
}
function parseStart(item, date) {
return dayjs.tz(
`${date.format('YYYY-MM-DD')} ${item.time}`,
'YYYY-MM-DD HH:mm A',
'America/New_York'
)
}
function parseItems(content, channel, date) {
const day = date.isoWeekday()
const $ = cheerio.load(content)
const table = $.html($(`#${channel.site_id} table`))
let data = table2array(table)
data.splice(0, 5)
return data.map(row => {
return {
time: row[1],
title: row[day + 1]
}
})
}
const table2array = require('table2array')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const isoWeek = require('dayjs/plugin/isoWeek')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
dayjs.extend(isoWeek)
module.exports = {
site: 'getafteritmedia.com',
days: 2,
url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml',
parser({ content, channel, date }) {
const programs = []
const items = parseItems(content, channel, date)
items.forEach(item => {
const prev = programs[programs.length - 1]
let start = parseStart(item, date)
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: item.title,
start,
stop
})
})
return programs
}
}
function parseStart(item, date) {
return dayjs.tz(
`${date.format('YYYY-MM-DD')} ${item.time}`,
'YYYY-MM-DD HH:mm A',
'America/New_York'
)
}
function parseItems(content, channel, date) {
const day = date.isoWeekday()
const $ = cheerio.load(content)
const table = $.html($(`#${channel.site_id} table`))
let data = table2array(table)
data.splice(0, 5)
return data.map(row => {
return {
time: row[1],
title: row[day + 1]
}
})
}

View File

@@ -1,45 +1,45 @@
const { parser, url } = require('./getafteritmedia.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-11-26', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '494637005',
xmltv_id: 'REVNWebFeed.us'
}
it('can generate valid url', () => {
expect(url).toBe(
'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-26T05:00:00.000Z',
stop: '2022-11-26T05:30:00.000Z',
title: 'The Appraisers'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./getafteritmedia.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-11-26', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '494637005',
xmltv_id: 'REVNWebFeed.us'
}
it('can generate valid url', () => {
expect(url).toBe(
'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-26T05:00:00.000Z',
stop: '2022-11-26T05:30:00.000Z',
title: 'The Appraisers'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})

View File

@@ -1,65 +1,65 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'gigatv.3bbtv.co.th',
days: 1,
url({ channel }) {
return `https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/${channel.site_id}.json`
},
parser: function ({ content, date }) {
let programs = []
const items = parseItems(content, date)
items.forEach(item => {
programs.push({
title: item.programName,
start: parseTime(item.startTime),
stop: parseTime(item.endTime)
})
})
return programs
},
async channels() {
const data = await axios
.get('https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/channel.json')
.then(r => r.data)
.catch(console.log)
const channels = []
data.forEach(group => {
group.channel_list.forEach(channel => {
channels.push({
lang: 'th',
site_id: channel.channel_id,
name: channel.channel_name
})
})
})
return channels
}
}
function parseTime(string) {
return dayjs.tz(string, 'YYYY-MM-DD HH:mm:ss', 'Asia/Bangkok')
}
function parseItems(content, date) {
try {
let data = JSON.parse(content)
if (!Array.isArray(data)) return []
data = data.filter(p => date.isSame(parseTime(p.startTime), 'day'))
return data
} catch {
return []
}
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'gigatv.3bbtv.co.th',
days: 1,
url({ channel }) {
return `https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/${channel.site_id}.json`
},
parser: function ({ content, date }) {
let programs = []
const items = parseItems(content, date)
items.forEach(item => {
programs.push({
title: item.programName,
start: parseTime(item.startTime),
stop: parseTime(item.endTime)
})
})
return programs
},
async channels() {
const data = await axios
.get('https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/channel.json')
.then(r => r.data)
.catch(console.log)
const channels = []
data.forEach(group => {
group.channel_list.forEach(channel => {
channels.push({
lang: 'th',
site_id: channel.channel_id,
name: channel.channel_name
})
})
})
return channels
}
}
function parseTime(string) {
return dayjs.tz(string, 'YYYY-MM-DD HH:mm:ss', 'Asia/Bangkok')
}
function parseItems(content, date) {
try {
let data = JSON.parse(content)
if (!Array.isArray(data)) return []
data = data.filter(p => date.isSame(parseTime(p.startTime), 'day'))
return data
} catch {
return []
}
}

View File

@@ -1,45 +1,45 @@
const { parser, url } = require('./gigatv.3bbtv.co.th.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '222',
xmltv_id: 'ThainessTV.th'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe(
'https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/222.json'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(32)
expect(results[0]).toMatchObject({
start: '2025-01-12T00:00:00.000Z',
stop: '2025-01-12T00:30:00.000Z',
title: 'THAILAND FORM ABOVE : TAK'
})
expect(results[31]).toMatchObject({
start: '2025-01-12T23:30:00.000Z',
stop: '2025-01-13T00:00:00.000Z',
title: 'MAESA ELEPHANT CAMP'
})
})
it('can handle empty guide', () => {
expect(parser({ content: '', date })).toMatchObject([])
})
const { parser, url } = require('./gigatv.3bbtv.co.th.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '222',
xmltv_id: 'ThainessTV.th'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe(
'https://gigatv.3bbtv.co.th/wp-content/themes/changwattana/epg/222.json'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(32)
expect(results[0]).toMatchObject({
start: '2025-01-12T00:00:00.000Z',
stop: '2025-01-12T00:30:00.000Z',
title: 'THAILAND FORM ABOVE : TAK'
})
expect(results[31]).toMatchObject({
start: '2025-01-12T23:30:00.000Z',
stop: '2025-01-13T00:00:00.000Z',
title: 'MAESA ELEPHANT CAMP'
})
})
it('can handle empty guide', () => {
expect(parser({ content: '', date })).toMatchObject([])
})

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