Replaced LF endings with CRLF

This commit is contained in:
freearhey
2025-07-29 05:28:59 +03:00
parent 651850370a
commit ca219de82d
86 changed files with 6821 additions and 6821 deletions

View File

@@ -1,32 +1,32 @@
import { SiteConfig } from 'epg-grabber'
import { pathToFileURL } from 'url'
export class ConfigLoader {
async load(filepath: string): Promise<SiteConfig> {
const fileUrl = pathToFileURL(filepath).toString()
const config = (await import(fileUrl)).default
const defaultConfig = {
days: 1,
delay: 0,
output: 'guide.xml',
request: {
method: 'GET',
maxContentLength: 5242880,
timeout: 30000,
withCredentials: true,
jar: null,
responseType: 'arraybuffer',
cache: false,
headers: null,
data: null
},
maxConnections: 1,
site: undefined,
url: undefined,
parser: undefined,
channels: undefined
}
return { ...defaultConfig, ...config } as SiteConfig
}
}
import { SiteConfig } from 'epg-grabber'
import { pathToFileURL } from 'url'
export class ConfigLoader {
async load(filepath: string): Promise<SiteConfig> {
const fileUrl = pathToFileURL(filepath).toString()
const config = (await import(fileUrl)).default
const defaultConfig = {
days: 1,
delay: 0,
output: 'guide.xml',
request: {
method: 'GET',
maxContentLength: 5242880,
timeout: 30000,
withCredentials: true,
jar: null,
responseType: 'arraybuffer',
cache: false,
headers: null,
data: null
},
maxConnections: 1,
site: undefined,
url: undefined,
parser: undefined,
channels: undefined
}
return { ...defaultConfig, ...config } as SiteConfig
}
}

View File

@@ -1,56 +1,56 @@
const { parser, url } = require('./9tv.co.il.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-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'Channel9.il'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-06T04:30:00.000Z',
stop: '2022-03-06T07:10:00.000Z',
title: 'Слепая',
image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
description:
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
},
{
start: '2022-03-06T07:10:00.000Z',
stop: '2022-03-06T08:10:00.000Z',
image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
title: 'Орел и решка. Морской сезон',
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./9tv.co.il.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-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'Channel9.il'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-06T04:30:00.000Z',
stop: '2022-03-06T07:10:00.000Z',
title: 'Слепая',
image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
description:
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
},
{
start: '2022-03-06T07:10:00.000Z',
stop: '2022-03-06T08:10:00.000Z',
image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
title: 'Орел и решка. Морской сезон',
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,50 +1,50 @@
const { parser, url } = require('./allente.dk.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.dk/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content =
''
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./allente.dk.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.dk/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content =
''
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
})
expect(result).toMatchObject([])
})

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./allente.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.fi/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./allente.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.fi/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./allente.no.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.no/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./allente.no.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.no/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./allente.se.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./allente.se.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
image:
'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,59 +1,59 @@
const { parser, url } = require('./arianatelevision.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('2021-11-27', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'ArianaTVNational.af'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.arianatelevision.com/program-schedule/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-27T02:30:00.000Z',
stop: '2021-11-27T03:00:00.000Z',
title: 'City Report'
},
{
start: '2021-11-27T03:00:00.000Z',
stop: '2021-11-27T10:30:00.000Z',
title: 'ICC T20 Highlights'
},
{
start: '2021-11-27T10:30:00.000Z',
stop: '2021-11-28T02:00:00.000Z',
title: 'ICC T20 World Cup'
},
{
start: '2021-11-28T02:00:00.000Z',
stop: '2021-11-28T02:30:00.000Z',
title: 'Quran and Hadis'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./arianatelevision.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('2021-11-27', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'ArianaTVNational.af'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.arianatelevision.com/program-schedule/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-27T02:30:00.000Z',
stop: '2021-11-27T03:00:00.000Z',
title: 'City Report'
},
{
start: '2021-11-27T03:00:00.000Z',
stop: '2021-11-27T10:30:00.000Z',
title: 'ICC T20 Highlights'
},
{
start: '2021-11-27T10:30:00.000Z',
stop: '2021-11-28T02:00:00.000Z',
title: 'ICC T20 World Cup'
},
{
start: '2021-11-28T02:00:00.000Z',
stop: '2021-11-28T02:30:00.000Z',
title: 'Quran and Hadis'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,66 +1,66 @@
const { parser, url, request } = require('./artonline.tv.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 channel = {
site_id: '#Aflam2',
xmltv_id: 'ARTAflam2.sa'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2')
})
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 for today', () => {
const date = dayjs.utc().startOf('d')
const data = request.data({ date })
expect(data.get('objId')).toBe('0')
})
it('can generate valid request data for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
const data = request.data({ date })
expect(data.get('objId')).toBe('1')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-03T21:30:00.000Z',
stop: '2022-03-03T23:04:00.000Z',
title: 'الراقصه و السياسي',
description:
'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .',
image: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: ''
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./artonline.tv.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 channel = {
site_id: '#Aflam2',
xmltv_id: 'ARTAflam2.sa'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2')
})
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 for today', () => {
const date = dayjs.utc().startOf('d')
const data = request.data({ date })
expect(data.get('objId')).toBe('0')
})
it('can generate valid request data for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
const data = request.data({ date })
expect(data.get('objId')).toBe('1')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-03T21:30:00.000Z',
stop: '2022-03-03T23:04:00.000Z',
title: 'الراقصه و السياسي',
description:
'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .',
image: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: ''
})
expect(result).toMatchObject([])
})

View File

@@ -1,42 +1,42 @@
const { parser, url } = require('./chada.ma.config.js')
const fs = require('fs')
const path = require('path')
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)
jest.mock('axios')
it('can generate valid url', () => {
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content }).map(p => {
p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
return p
})
expect(result).toMatchObject([
{
title: 'Bloc Prime + Clips',
description: 'No description available',
start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'),
stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./chada.ma.config.js')
const fs = require('fs')
const path = require('path')
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)
jest.mock('axios')
it('can generate valid url', () => {
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content }).map(p => {
p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
return p
})
expect(result).toMatchObject([
{
title: 'Bloc Prime + Clips',
description: 'No description available',
start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'),
stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./chaines-tv.orange.fr.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('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '192',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
const result = url({ channel, date })
expect(result).toBe(
'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste',
subTitle: 'Esprits criminels',
season: 10,
episode: 12,
description:
"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.",
category: 'Série Suspense',
image: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./chaines-tv.orange.fr.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('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '192',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
const result = url({ channel, date })
expect(result).toBe(
'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste',
subTitle: 'Esprits criminels',
season: 10,
episode: 12,
description:
"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.",
category: 'Série Suspense',
image: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,55 +1,55 @@
const { parser, url } = require('./cosmotetv.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')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
jest.mock('axios')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
it('can generate valid url', () => {
const startOfDay = dayjs(date).startOf('day').utc().unix()
const endOfDay = dayjs(date).endOf('day').utc().unix()
expect(url({ date, channel })).toBe(
`https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const result = parser({ date, content }).map(p => {
p.start = dayjs(p.start).toISOString()
p.stop = dayjs(p.stop).toISOString()
return p
})
expect(result).toMatchObject([
{
title: 'Τι Λέει ο Νόμος',
description:
'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
category: 'Special',
image:
'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg',
start: '2024-12-26T23:00:00.000Z',
stop: '2024-12-27T00:00:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./cosmotetv.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')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
jest.mock('axios')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
it('can generate valid url', () => {
const startOfDay = dayjs(date).startOf('day').utc().unix()
const endOfDay = dayjs(date).endOf('day').utc().unix()
expect(url({ date, channel })).toBe(
`https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const result = parser({ date, content }).map(p => {
p.start = dayjs(p.start).toISOString()
p.stop = dayjs(p.stop).toISOString()
return p
})
expect(result).toMatchObject([
{
title: 'Τι Λέει ο Νόμος',
description:
'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
category: 'Special',
image:
'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg',
start: '2024-12-26T23:00:00.000Z',
stop: '2024-12-27T00:00:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,47 +1,47 @@
const { url, parser } = require('./cubmu.com.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('2023-11-05', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' }
const channelEn = Object.assign({}, channel, { lang: 'en' })
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=2023-11-05&channel_id=4028c68574537fcd0174be43042758d8'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const idResults = parser({ content, channel })
expect(idResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
description:
'CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.'
}
])
const enResults = parser({ content, channel: channelEn })
expect(enResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
description:
'CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
const { url, parser } = require('./cubmu.com.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('2023-11-05', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' }
const channelEn = Object.assign({}, channel, { lang: 'en' })
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=2023-11-05&channel_id=4028c68574537fcd0174be43042758d8'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const idResults = parser({ content, channel })
expect(idResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
description:
'CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.'
}
])
const enResults = parser({ content, channel: channelEn })
expect(enResults).toMatchObject([
{
start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News',
description:
'CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})

View File

@@ -1,48 +1,48 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const parseDuration = require('parse-duration').default
const timezone = require('dayjs/plugin/timezone')
const { sortBy } = require('../../scripts/functions')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
module.exports = {
site: 'derana.lk',
url({ date }) {
return `https://derana.lk/api/schedules/${date.format('DD-MM-YYYY')}`
},
parser({ content }) {
const programs = parseItems(content).map(item => {
const start = parseStart(item)
const duration = parseDuration(item.duration)
const stop = start.add(duration, 'ms')
return {
title: item.dramaName,
image: item.imageUrl,
start,
stop
}
})
return sortBy(programs, p => p.start.valueOf())
}
}
function parseStart(item) {
return dayjs.tz(`${item.date} ${item.time}`, 'DD-MM-YYYY H:mm A', 'Asia/Colombo')
}
function parseItems(content) {
try {
const data = JSON.parse(content)
if (!data || !Array.isArray(data.all_schedules)) return []
return data.all_schedules
} catch {
return []
}
}
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const parseDuration = require('parse-duration').default
const timezone = require('dayjs/plugin/timezone')
const { sortBy } = require('../../scripts/functions')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
module.exports = {
site: 'derana.lk',
url({ date }) {
return `https://derana.lk/api/schedules/${date.format('DD-MM-YYYY')}`
},
parser({ content }) {
const programs = parseItems(content).map(item => {
const start = parseStart(item)
const duration = parseDuration(item.duration)
const stop = start.add(duration, 'ms')
return {
title: item.dramaName,
image: item.imageUrl,
start,
stop
}
})
return sortBy(programs, p => p.start.valueOf())
}
}
function parseStart(item) {
return dayjs.tz(`${item.date} ${item.time}`, 'DD-MM-YYYY H:mm A', 'Asia/Colombo')
}
function parseItems(content) {
try {
const data = JSON.parse(content)
if (!data || !Array.isArray(data.all_schedules)) return []
return data.all_schedules
} catch {
return []
}
}

View File

@@ -1,78 +1,78 @@
const { parser, url, request } = require('./directv.com.ar.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-06-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '207#A&amp;EHD',
xmltv_id: 'AEHDSouth.us'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.ar/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=16; PGLang=S; PGCulture=es-AR;'
})
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
filterParameters: {
day: 19,
time: 0,
minute: 0,
month: 6,
year: 2022,
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum: '207',
channelName: 'A&EHD'
}
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-06-19T03:00:00.000Z',
stop: '2022-06-19T03:15:00.000Z',
title: 'Chicas guapas',
description:
'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.',
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.ar.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-06-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '207#A&amp;EHD',
xmltv_id: 'AEHDSouth.us'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.ar/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=16; PGLang=S; PGCulture=es-AR;'
})
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
filterParameters: {
day: 19,
time: 0,
minute: 0,
month: 6,
year: 2022,
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum: '207',
channelName: 'A&EHD'
}
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-06-19T03:00:00.000Z',
stop: '2022-06-19T03:15:00.000Z',
title: 'Chicas guapas',
description:
'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.',
rating: {
system: 'MPA',
value: 'NR'
}
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '',
channel
})
expect(result).toMatchObject([])
})

View File

@@ -1,206 +1,206 @@
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')
const { uniqBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide'
module.exports = {
site: 'dstv.com',
days: 2,
request: {
cache: {
ttl: 3 * 60 * 60 * 1000, // 3h
interpretHeader: false
}
},
url: function ({ channel, date }) {
const [region] = channel.site_id.split('#')
const packageName = region === 'nga' ? '&package=DStv%20Premium' : ''
return `${API_ENDPOINT}/GetProgrammes?d=${date.format(
'YYYY-MM-DD'
)}${packageName}&country=${region}`
},
async parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
for (const item of items) {
const details = await loadProgramDetails(item)
programs.push({
title: item.Title,
description: parseDescription(details),
image: parseImage(details),
category: parseCategory(details),
start: parseTime(item.StartTime, channel),
stop: parseTime(item.EndTime, channel)
})
}
return programs
},
async channels({ country }) {
const countries = {
ao: 'ago',
bj: 'ben',
bw: 'bwa',
bf: 'bfa',
bi: 'bdi',
cm: 'cmr',
cv: 'cpv',
td: 'tcd',
cf: 'caf',
km: 'com',
cd: 'cod',
dj: 'dji',
gq: 'gnq',
er: 'eri',
sz: 'swz',
et: 'eth',
ga: 'gab',
gm: 'gmb',
gh: 'gha',
gn: 'gin',
gw: 'gnb',
ci: 'civ',
ke: 'ken',
lr: 'lbr',
mg: 'mdg',
mw: 'mwi',
ml: 'mli',
mr: 'mrt',
mu: 'mus',
mz: 'moz',
na: 'nam',
ne: 'ner',
ng: 'nga',
cg: 'cog',
rw: 'rwa',
st: 'stp',
sn: 'sen',
sc: 'syc',
sl: 'sle',
so: 'som',
za: 'zaf',
ss: 'ssd',
sd: 'sdn',
tz: 'tza',
tg: 'tgo',
ug: 'uga',
zm: 'zmb',
zw: 'zwe'
}
const code = countries[country]
const data = await axios
.get(`${API_ENDPOINT}/GetProgrammes?d=${dayjs().format('YYYY-MM-DD')}&country=${code}`)
.then(r => r.data)
.catch(console.log)
let channels = []
data.Channels.forEach(item => {
channels.push({
lang: 'en',
site_id: `${code}#${item.Number}`,
name: item.Name
})
})
return uniqBy(channels, 'site_id')
}
}
function parseTime(time, channel) {
const tz = {
ago: 'Africa/Luanda',
ben: 'Africa/Porto-Novo',
bwa: 'Africa/Gaborone',
bfa: 'Africa/Ouagadougou',
bdi: 'Africa/Bujumbura',
cmr: 'Africa/Douala',
cpv: 'CVT',
tcd: 'Africa/Ndjamena',
caf: 'Africa/Bangui',
com: 'Indian/Comoro',
cod: 'Africa/Kinshasa',
dji: 'Africa/Djibouti',
gnq: 'Africa/Malabo',
eri: 'Africa/Asmara',
swz: 'SAST',
eth: 'Africa/Addis_Ababa',
gap: 'Africa/Libreville',
gmb: 'Africa/Banjul',
gha: 'Africa/Accra',
gin: 'Africa/Conakry',
gnb: 'Africa/Bissau',
civ: 'Africa/Abidjan',
ken: 'Africa/Nairobi',
lbr: 'Africa/Monrovia',
mdg: 'Indian/Antananarivo',
mwi: 'Africa/Blantyre',
mli: 'Africa/Bamako',
mrt: 'Africa/Nouakchott',
mus: 'Indian/Mauritius',
moz: 'Africa/Maputo',
nam: 'Africa/Windhoek',
ner: 'Africa/Niamey',
nga: 'Africa/Lagos',
cog: 'Africa/Brazzaville',
rwa: 'Africa/Kigali',
stp: 'Africa/Sao_Tome',
sen: 'Africa/Dakar',
syc: 'Indian/Mahe',
sle: 'Africa/Freetown',
som: 'Africa/Mogadishu',
zaf: 'Africa/Johannesburg',
ssd: 'Africa/Juba',
sdn: 'Africa/Khartoum',
tza: 'Africa/Dar_es_Salaam',
tgo: 'Africa/Lome',
uga: 'Africa/Kampala',
zmb: 'Africa/Lusaka',
zwe: 'Africa/Harare'
}
const [region] = channel.site_id.split('#')
return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region])
}
function parseDescription(details) {
return details ? details.Synopsis : null
}
function parseImage(details) {
return details ? details.ThumbnailUri : null
}
function parseCategory(details) {
return details ? details.SubGenres : null
}
async function loadProgramDetails(item) {
const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}`
return axios
.get(url)
.then(r => r.data)
.catch(console.error)
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !Array.isArray(data.Channels)) return []
const channelData = data.Channels.find(c => c.Number === channelId)
if (!channelData || !Array.isArray(channelData.Programmes)) return []
return channelData.Programmes
}
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')
const { uniqBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide'
module.exports = {
site: 'dstv.com',
days: 2,
request: {
cache: {
ttl: 3 * 60 * 60 * 1000, // 3h
interpretHeader: false
}
},
url: function ({ channel, date }) {
const [region] = channel.site_id.split('#')
const packageName = region === 'nga' ? '&package=DStv%20Premium' : ''
return `${API_ENDPOINT}/GetProgrammes?d=${date.format(
'YYYY-MM-DD'
)}${packageName}&country=${region}`
},
async parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
for (const item of items) {
const details = await loadProgramDetails(item)
programs.push({
title: item.Title,
description: parseDescription(details),
image: parseImage(details),
category: parseCategory(details),
start: parseTime(item.StartTime, channel),
stop: parseTime(item.EndTime, channel)
})
}
return programs
},
async channels({ country }) {
const countries = {
ao: 'ago',
bj: 'ben',
bw: 'bwa',
bf: 'bfa',
bi: 'bdi',
cm: 'cmr',
cv: 'cpv',
td: 'tcd',
cf: 'caf',
km: 'com',
cd: 'cod',
dj: 'dji',
gq: 'gnq',
er: 'eri',
sz: 'swz',
et: 'eth',
ga: 'gab',
gm: 'gmb',
gh: 'gha',
gn: 'gin',
gw: 'gnb',
ci: 'civ',
ke: 'ken',
lr: 'lbr',
mg: 'mdg',
mw: 'mwi',
ml: 'mli',
mr: 'mrt',
mu: 'mus',
mz: 'moz',
na: 'nam',
ne: 'ner',
ng: 'nga',
cg: 'cog',
rw: 'rwa',
st: 'stp',
sn: 'sen',
sc: 'syc',
sl: 'sle',
so: 'som',
za: 'zaf',
ss: 'ssd',
sd: 'sdn',
tz: 'tza',
tg: 'tgo',
ug: 'uga',
zm: 'zmb',
zw: 'zwe'
}
const code = countries[country]
const data = await axios
.get(`${API_ENDPOINT}/GetProgrammes?d=${dayjs().format('YYYY-MM-DD')}&country=${code}`)
.then(r => r.data)
.catch(console.log)
let channels = []
data.Channels.forEach(item => {
channels.push({
lang: 'en',
site_id: `${code}#${item.Number}`,
name: item.Name
})
})
return uniqBy(channels, 'site_id')
}
}
function parseTime(time, channel) {
const tz = {
ago: 'Africa/Luanda',
ben: 'Africa/Porto-Novo',
bwa: 'Africa/Gaborone',
bfa: 'Africa/Ouagadougou',
bdi: 'Africa/Bujumbura',
cmr: 'Africa/Douala',
cpv: 'CVT',
tcd: 'Africa/Ndjamena',
caf: 'Africa/Bangui',
com: 'Indian/Comoro',
cod: 'Africa/Kinshasa',
dji: 'Africa/Djibouti',
gnq: 'Africa/Malabo',
eri: 'Africa/Asmara',
swz: 'SAST',
eth: 'Africa/Addis_Ababa',
gap: 'Africa/Libreville',
gmb: 'Africa/Banjul',
gha: 'Africa/Accra',
gin: 'Africa/Conakry',
gnb: 'Africa/Bissau',
civ: 'Africa/Abidjan',
ken: 'Africa/Nairobi',
lbr: 'Africa/Monrovia',
mdg: 'Indian/Antananarivo',
mwi: 'Africa/Blantyre',
mli: 'Africa/Bamako',
mrt: 'Africa/Nouakchott',
mus: 'Indian/Mauritius',
moz: 'Africa/Maputo',
nam: 'Africa/Windhoek',
ner: 'Africa/Niamey',
nga: 'Africa/Lagos',
cog: 'Africa/Brazzaville',
rwa: 'Africa/Kigali',
stp: 'Africa/Sao_Tome',
sen: 'Africa/Dakar',
syc: 'Indian/Mahe',
sle: 'Africa/Freetown',
som: 'Africa/Mogadishu',
zaf: 'Africa/Johannesburg',
ssd: 'Africa/Juba',
sdn: 'Africa/Khartoum',
tza: 'Africa/Dar_es_Salaam',
tgo: 'Africa/Lome',
uga: 'Africa/Kampala',
zmb: 'Africa/Lusaka',
zwe: 'Africa/Harare'
}
const [region] = channel.site_id.split('#')
return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region])
}
function parseDescription(details) {
return details ? details.Synopsis : null
}
function parseImage(details) {
return details ? details.ThumbnailUri : null
}
function parseCategory(details) {
return details ? details.SubGenres : null
}
async function loadProgramDetails(item) {
const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}`
return axios
.get(url)
.then(r => r.data)
.catch(console.error)
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !Array.isArray(data.Channels)) return []
const channelData = data.Channels.find(c => c.Number === channelId)
if (!channelData || !Array.isArray(channelData.Programmes)) return []
return channelData.Programmes
}

View File

@@ -1,38 +1,38 @@
const { url, parser } = require('./firstmedia.com.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('2023-11-08').startOf('d')
const channel = { site_id: '243', xmltv_id: 'AlJazeeraEnglish.qa', lang: 'id' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://api.firstmedia.com/api/content/tv-guide/list?date=08/11/2023&channel=243&startTime=1&endTime=24'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const results = parser({ content, channel, date })
// All time in Asia/Jakarta
// 2023-11-08 17:00:00 -> 2023-11-08 20:00:00 = 2023-11-08 03:00:00
// 2023-11-08 17:00:00 -> 2023-11-08 20:30:00 = 2023-11-08 03:30:00
expect(results).toMatchObject([
{
start: '2023-11-07T20:00:00.000Z',
stop: '2023-11-07T20:30:00.000Z',
title: 'News Live',
description: 'Up to date news and analysis from around the world.'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
const { url, parser } = require('./firstmedia.com.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('2023-11-08').startOf('d')
const channel = { site_id: '243', xmltv_id: 'AlJazeeraEnglish.qa', lang: 'id' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://api.firstmedia.com/api/content/tv-guide/list?date=08/11/2023&channel=243&startTime=1&endTime=24'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const results = parser({ content, channel, date })
// All time in Asia/Jakarta
// 2023-11-08 17:00:00 -> 2023-11-08 20:00:00 = 2023-11-08 03:00:00
// 2023-11-08 17:00:00 -> 2023-11-08 20:30:00 = 2023-11-08 03:30:00
expect(results).toMatchObject([
{
start: '2023-11-07T20:00:00.000Z',
stop: '2023-11-07T20:30:00.000Z',
title: 'News Live',
description: 'Up to date news and analysis from around the world.'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})

View File

@@ -1,43 +1,43 @@
const { parser, url } = require('./foxsports.com.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('2022-12-14', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2',
xmltv_id: 'FoxLeague.au'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'NRL',
sub_title: 'Eels v Titans',
description:
'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.',
category: 'Rugby League',
start: '2022-12-13T13:00:00.000Z',
stop: '2022-12-13T14:00:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({content: ''}, channel)
expect(result).toMatchObject([])
})
const { parser, url } = require('./foxsports.com.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('2022-12-14', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2',
xmltv_id: 'FoxLeague.au'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'NRL',
sub_title: 'Eels v Titans',
description:
'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.',
category: 'Rugby League',
start: '2022-12-13T13:00:00.000Z',
stop: '2022-12-13T14:00:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({content: ''}, channel)
expect(result).toMatchObject([])
})

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./freetv.tv.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-03-28', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '3370462',
xmltv_id: 'Kan11.il'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://web.freetv.tv/api/products/lives/programmes?liveId[]=3370462&since=2025-03-28T00%3A00%2B0200&till=2025-03-29T00%3A00%2B0300&lang=HEB&platform=BROWSER')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(2)
expect(results[0]).toMatchObject({
title: 'בוש 4 - פרק 3',
description: 'עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב',
image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
start: '2025-03-27T21:26:00.000Z',
stop: '2025-03-27T22:17:00.000Z'
})
expect(results[1]).toMatchObject({
title: 'אבא משתדל - 5. חבר',
description: 'סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.',
image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
start: '2025-03-27T22:17:00.000Z',
stop: '2025-03-27T22:43:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
const { parser, url } = require('./freetv.tv.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-03-28', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '3370462',
xmltv_id: 'Kan11.il'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://web.freetv.tv/api/products/lives/programmes?liveId[]=3370462&since=2025-03-28T00%3A00%2B0200&till=2025-03-29T00%3A00%2B0300&lang=HEB&platform=BROWSER')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(2)
expect(results[0]).toMatchObject({
title: 'בוש 4 - פרק 3',
description: 'עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב',
image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
start: '2025-03-27T21:26:00.000Z',
stop: '2025-03-27T22:17:00.000Z'
})
expect(results[1]).toMatchObject({
title: 'אבא משתדל - 5. חבר',
description: 'סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.',
image: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
icon: 'https://d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1',
start: '2025-03-27T22:17:00.000Z',
stop: '2025-03-27T22:43:00.000Z'
})
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})

View File

@@ -1,48 +1,48 @@
const { parser, url } = require('./frikanalen.no.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-01-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'Frikanalen.no'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-18T23:47:00.000Z',
stop: '2022-01-19T00:44:55.640Z',
title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik',
category: ['Samfunn'],
description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS."
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./frikanalen.no.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-01-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'Frikanalen.no'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-18T23:47:00.000Z',
stop: '2022-01-19T00:44:55.640Z',
title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik',
category: ['Samfunn'],
description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS."
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,52 +1,52 @@
const { parser, url } = require('./galamtv.kz.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-10', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '636e54cf8a8f73bae8244f41',
xmltv_id: 'Qazaqstan.kz'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
`https://galam.server-api.lfstrm.tv/channels/${
channel.site_id
}/programs?period=${date.unix()}:${date.add(1, 'day').unix()}`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2025-01-10T01:00:00.000Z',
stop: '2025-01-10T01:05:00.000Z',
title: 'Гимн',
description: 'Государственный гимн Республики Казахстан',
image:
'http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./galamtv.kz.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-10', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '636e54cf8a8f73bae8244f41',
xmltv_id: 'Qazaqstan.kz'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
`https://galam.server-api.lfstrm.tv/channels/${
channel.site_id
}/programs?period=${date.unix()}:${date.add(1, 'day').unix()}`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2025-01-10T01:00:00.000Z',
stop: '2025-01-10T01:05:00.000Z',
title: 'Гимн',
description: 'Государственный гимн Республики Казахстан',
image:
'http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,98 +1,98 @@
const axios = require('axios')
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 { uniqBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'guida.tv',
days: 2,
url: function ({ date, channel }) {
return `https://www.guida.tv/programmi-tv/palinsesto/canale/${
channel.site_id
}.html?dt=${date.format('YYYY-MM-DD')}`
},
parser: function ({ content, date, channel }) {
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, channel)
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
},
async channels() {
const providers = ['-1', '-2', '-3']
const channels = []
for (let provider of providers) {
const data = await axios
.post('https://www.guida.tv/guide/schedule', null, {
params: {
provider,
region: 'Italy',
TVperiod: 'Night',
date: dayjs().format('YYYY-MM-DD'),
st: 0,
u_time: 1429,
is_mobile: 1
}
})
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
$('.channelname').each((i, el) => {
const name = $(el).find('center > a:eq(1)').text()
const url = $(el).find('center > a:eq(1)').attr('href')
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
channels.push({
lang: 'it',
name,
site_id: `${number}/${slug}`
})
})
}
return uniqBy(channels, 'site_id')
}
}
function parseStart($item, date) {
const timeString = $item('td:eq(0)').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Rome')
}
function parseTitle($item) {
return $item('td:eq(1)').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('table.table > tbody > tr').toArray()
}
const axios = require('axios')
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 { uniqBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'guida.tv',
days: 2,
url: function ({ date, channel }) {
return `https://www.guida.tv/programmi-tv/palinsesto/canale/${
channel.site_id
}.html?dt=${date.format('YYYY-MM-DD')}`
},
parser: function ({ content, date, channel }) {
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, channel)
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
},
async channels() {
const providers = ['-1', '-2', '-3']
const channels = []
for (let provider of providers) {
const data = await axios
.post('https://www.guida.tv/guide/schedule', null, {
params: {
provider,
region: 'Italy',
TVperiod: 'Night',
date: dayjs().format('YYYY-MM-DD'),
st: 0,
u_time: 1429,
is_mobile: 1
}
})
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
$('.channelname').each((i, el) => {
const name = $(el).find('center > a:eq(1)').text()
const url = $(el).find('center > a:eq(1)').attr('href')
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
channels.push({
lang: 'it',
name,
site_id: `${number}/${slug}`
})
})
}
return uniqBy(channels, 'site_id')
}
}
function parseStart($item, date) {
const timeString = $item('td:eq(0)').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Rome')
}
function parseTitle($item) {
return $item('td:eq(1)').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('table.table > tbody > tr').toArray()
}

View File

@@ -1,52 +1,52 @@
const { parser, url } = require('./guidatv.sky.it.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-05-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'DTH#10458',
xmltv_id: '20Mediaset.it'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-05-06T00:35:40.000Z',
stop: '2022-05-06T01:15:40.000Z',
title: 'Distretto di Polizia',
description:
"S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.",
season: 6,
episode: 26,
image:
'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b',
category: 'Intrattenimento/Fiction',
url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./guidatv.sky.it.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-05-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'DTH#10458',
xmltv_id: '20Mediaset.it'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-05-06T00:35:40.000Z',
stop: '2022-05-06T01:15:40.000Z',
title: 'Distretto di Polizia',
description:
"S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.",
season: 6,
episode: 26,
image:
'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b',
category: 'Intrattenimento/Fiction',
url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,338 +1,338 @@
const cheerio = require('cheerio')
const axios = require('axios')
const dayjs = require('dayjs')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
require('dayjs/locale/fr')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
const PARIS_TZ = 'Europe/Paris'
module.exports = {
site: 'guidetnt.com',
days: 2,
url({ channel, date }) {
const now = dayjs()
const demain = now.add(1, 'd')
if (date && date.isSame(demain, 'day')) {
return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}`
} else if (!date || date.isSame(now, 'day')) {
return `https://www.guidetnt.com/tv/programme-${channel.site_id}`
} else {
return null
}
},
async parser({ content, date }) {
const programs = []
const allItems = parseItems(content)
const items = allItems?.rows
const itemDate = allItems?.formattedDate
for (const item of items) {
const prev = programs[programs.length - 1]
const $item = cheerio.load(item)
const title = parseTitle($item)
let start = parseStart($item, itemDate)
if (!start || !title) return
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
let stop = start.add(30, 'm')
let itemDetails = null
let subTitle = null
//let duration = null
let country = null
let productionDate = null
let episode = null
let season = null
let category = parseCategory($item)
let description = parseDescription($item)
const itemDetailsURL = parseDescriptionURL($item)
if (itemDetailsURL) {
const url = 'https://www.guidetnt.com' + itemDetailsURL
try {
const response = await axios.get(url)
itemDetails = parseItemDetails(response.data)
} catch (err) {
console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, err)
}
const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD'))
start = timeRange?.start
stop = timeRange?.stop
subTitle = itemDetails?.subTitle
if (title == subTitle) subTitle = null
description = itemDetails?.description
const categoryDetails = parseCategoryText(itemDetails?.category)
//duration = categoryDetails?.duration
country = categoryDetails?.country
productionDate = categoryDetails?.productionDate
season = categoryDetails?.season
episode = categoryDetails?.episode
}
// See https://www.npmjs.com/package/epg-parser for parameters
programs.push({
title,
subTitle: subTitle,
description: description,
image: itemDetails?.image,
category: category,
directors: itemDetails?.directorActors?.Réalisateur,
actors: itemDetails?.directorActors?.Acteur,
country: country,
date: productionDate,
//duration: duration, // Tried with length: too, but does not work ! (stop-start is not accurate because of Ads)
season: season,
episode: episode,
start,
stop
})
}
return programs
},
async channels() {
const response = await axios.get('https://www.guidetnt.com')
const channels = []
const $ = cheerio.load(response.data)
// Look inside each .tvlogo container
$('.tvlogo').each((i, el) => {
// Find all descendants that have an alt attribute
$(el)
.find('[alt]')
.each((j, subEl) => {
const alt = $(subEl).attr('alt')
const href = $(subEl).attr('href')
if (href && alt && alt.trim() !== '') {
const name = alt.trim()
const site_id = href.replace(/^\/tv\/programme-/, '')
channels.push({
lang: 'fr',
name,
site_id
})
}
})
})
return channels
}
}
function parseTimeRange(timeRange, baseDate) {
// Split times
const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim())
// Parse with base date
const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm')
let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm')
// Handle possible day wrap (e.g., 23:30 - 00:15)
if (end.isBefore(start)) {
end = end.add(1, 'day')
}
// Calculate duration in minutes
const diffMinutes = end.diff(start, 'minute')
return {
start: start.format(),
stop: end.format(),
duration: diffMinutes
}
}
function parseItemDetails(itemDetails) {
const $ = cheerio.load(itemDetails)
const program = $('.program-wrapper').first()
const programHour = program.find('.program-hour').text().trim()
const programTitle = program.find('.program-title').text().trim()
const programElementBold = program.find('.program-element-bold').text().trim()
const programArea1 = program.find('.program-element.program-area-1').text().trim()
let description = ''
const programElements = $('.program-element').filter((i, el) => {
const classAttr = $(el).attr('class')
// Return true only if it is exactly "program-element" (no extra classes)
return classAttr.trim() === 'program-element'
})
programElements.each((i, el) => {
description += $(el).text().trim()
})
const area2Node = $('.program-area-2').first()
const area2 = $(area2Node)
const data = {}
let currentLabel = null
let texts = []
area2.contents().each((i, node) => {
if (node.type === 'tag' && node.name === 'strong') {
// If we had collected some text for the previous label, save it
if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma
}
// New label - get text without colon
currentLabel = $(node).text().replace(/:$/, '').trim()
texts = []
} else if (currentLabel) {
// Append the text content (text node or others)
if (node.type === 'text') {
texts.push(node.data)
} else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') {
texts.push($(node).text())
}
}
})
// Save last label text
if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '')
}
const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null
return {
programHour,
title: programTitle,
subTitle: programElementBold,
category: programArea1,
description: description,
directorActors: data,
image: imgSrc
}
}
function parseCategoryText(text) {
if (!text) return null
const parts = text
.split(',')
.map(s => s.trim())
.filter(Boolean)
const len = parts.length
const category = parts[0] || null
if (len < 3) {
return {
category: category,
duration: null,
country: null,
productionDate: null,
season: null,
episode: null
}
}
// Check last part: date if numeric
const dateCandidate = parts[len - 1]
const productionDate = /^\d{4}$/.test(dateCandidate) ? dateCandidate : null
// Check for duration (first part containing "minutes")
let durationMinute = null
//let duration = null
let episode = null
let season = null
let durationIndex = -1
for (let i = 0; i < len; i++) {
if (parts[i].toLowerCase().includes('minute')) {
durationMinute = parts[i].trim()
durationMinute = durationMinute.replace('minutes', '')
durationMinute = durationMinute.replace('minute', '')
//duration = [{ units: 'minutes', value: durationMinute }],
durationIndex = i
} else if (parts[i].toLowerCase().includes('épisode')) {
const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i)
if (match) {
episode = parseInt(match[1], 10)
}
} else if (parts[i].toLowerCase().includes('saison')) {
season = parts[i].replace('saison', '').trim()
}
}
// Country: second to last
const countryIndex = len - 2
let country = durationIndex === countryIndex ? null : parts[countryIndex]
return {
category,
durationMinute,
country,
productionDate,
season,
episode
}
}
function parseTitle($item) {
return $item('.channel-programs-title a').text().trim()
}
function parseDescription($item) {
return $item('#descr').text().trim() || null
}
function parseDescriptionURL($item) {
const descrLink = $item('#descr a')
return descrLink.attr('href') || null
}
function parseCategory($item) {
let type = null
$item('.channel-programs-title span').each((i, span) => {
const className = $item(span).attr('class')
if (className && className.startsWith('text_bg')) {
type = $item(span).text().trim()
}
})
return type
}
function parseStart($item, itemDate) {
const dt = $item('.channel-programs-time a').text().trim()
if (!dt) return null
const datetimeStr = `${itemDate} ${dt}`
return dayjs.tz(datetimeStr, 'YYYY-MM-DD HH:mm', PARIS_TZ)
}
function parseItems(content) {
const $ = cheerio.load(content)
// Extract header information
const logoSrc = $('#logo img').attr('src')
const title = $('#title h1').text().trim()
const subtitle = $('#subtitle').text().trim()
const dateMatch = subtitle.match(/(\d{1,2} \w+ \d{4})/)
const dateStr = dateMatch ? dateMatch[1].toLowerCase() : null
// Parse the French date string
const parsedDate = dayjs(dateStr, 'D MMMM YYYY', 'fr')
// Format it as YYYY-MM-DD
const formattedDate = parsedDate.format('YYYY-MM-DD')
const rows = $('.channel-row').toArray()
return {
rows,
logoSrc,
title,
formattedDate
}
}
const cheerio = require('cheerio')
const axios = require('axios')
const dayjs = require('dayjs')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
require('dayjs/locale/fr')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
const PARIS_TZ = 'Europe/Paris'
module.exports = {
site: 'guidetnt.com',
days: 2,
url({ channel, date }) {
const now = dayjs()
const demain = now.add(1, 'd')
if (date && date.isSame(demain, 'day')) {
return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}`
} else if (!date || date.isSame(now, 'day')) {
return `https://www.guidetnt.com/tv/programme-${channel.site_id}`
} else {
return null
}
},
async parser({ content, date }) {
const programs = []
const allItems = parseItems(content)
const items = allItems?.rows
const itemDate = allItems?.formattedDate
for (const item of items) {
const prev = programs[programs.length - 1]
const $item = cheerio.load(item)
const title = parseTitle($item)
let start = parseStart($item, itemDate)
if (!start || !title) return
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
let stop = start.add(30, 'm')
let itemDetails = null
let subTitle = null
//let duration = null
let country = null
let productionDate = null
let episode = null
let season = null
let category = parseCategory($item)
let description = parseDescription($item)
const itemDetailsURL = parseDescriptionURL($item)
if (itemDetailsURL) {
const url = 'https://www.guidetnt.com' + itemDetailsURL
try {
const response = await axios.get(url)
itemDetails = parseItemDetails(response.data)
} catch (err) {
console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, err)
}
const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD'))
start = timeRange?.start
stop = timeRange?.stop
subTitle = itemDetails?.subTitle
if (title == subTitle) subTitle = null
description = itemDetails?.description
const categoryDetails = parseCategoryText(itemDetails?.category)
//duration = categoryDetails?.duration
country = categoryDetails?.country
productionDate = categoryDetails?.productionDate
season = categoryDetails?.season
episode = categoryDetails?.episode
}
// See https://www.npmjs.com/package/epg-parser for parameters
programs.push({
title,
subTitle: subTitle,
description: description,
image: itemDetails?.image,
category: category,
directors: itemDetails?.directorActors?.Réalisateur,
actors: itemDetails?.directorActors?.Acteur,
country: country,
date: productionDate,
//duration: duration, // Tried with length: too, but does not work ! (stop-start is not accurate because of Ads)
season: season,
episode: episode,
start,
stop
})
}
return programs
},
async channels() {
const response = await axios.get('https://www.guidetnt.com')
const channels = []
const $ = cheerio.load(response.data)
// Look inside each .tvlogo container
$('.tvlogo').each((i, el) => {
// Find all descendants that have an alt attribute
$(el)
.find('[alt]')
.each((j, subEl) => {
const alt = $(subEl).attr('alt')
const href = $(subEl).attr('href')
if (href && alt && alt.trim() !== '') {
const name = alt.trim()
const site_id = href.replace(/^\/tv\/programme-/, '')
channels.push({
lang: 'fr',
name,
site_id
})
}
})
})
return channels
}
}
function parseTimeRange(timeRange, baseDate) {
// Split times
const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim())
// Parse with base date
const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm')
let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm')
// Handle possible day wrap (e.g., 23:30 - 00:15)
if (end.isBefore(start)) {
end = end.add(1, 'day')
}
// Calculate duration in minutes
const diffMinutes = end.diff(start, 'minute')
return {
start: start.format(),
stop: end.format(),
duration: diffMinutes
}
}
function parseItemDetails(itemDetails) {
const $ = cheerio.load(itemDetails)
const program = $('.program-wrapper').first()
const programHour = program.find('.program-hour').text().trim()
const programTitle = program.find('.program-title').text().trim()
const programElementBold = program.find('.program-element-bold').text().trim()
const programArea1 = program.find('.program-element.program-area-1').text().trim()
let description = ''
const programElements = $('.program-element').filter((i, el) => {
const classAttr = $(el).attr('class')
// Return true only if it is exactly "program-element" (no extra classes)
return classAttr.trim() === 'program-element'
})
programElements.each((i, el) => {
description += $(el).text().trim()
})
const area2Node = $('.program-area-2').first()
const area2 = $(area2Node)
const data = {}
let currentLabel = null
let texts = []
area2.contents().each((i, node) => {
if (node.type === 'tag' && node.name === 'strong') {
// If we had collected some text for the previous label, save it
if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma
}
// New label - get text without colon
currentLabel = $(node).text().replace(/:$/, '').trim()
texts = []
} else if (currentLabel) {
// Append the text content (text node or others)
if (node.type === 'text') {
texts.push(node.data)
} else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') {
texts.push($(node).text())
}
}
})
// Save last label text
if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '')
}
const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null
return {
programHour,
title: programTitle,
subTitle: programElementBold,
category: programArea1,
description: description,
directorActors: data,
image: imgSrc
}
}
function parseCategoryText(text) {
if (!text) return null
const parts = text
.split(',')
.map(s => s.trim())
.filter(Boolean)
const len = parts.length
const category = parts[0] || null
if (len < 3) {
return {
category: category,
duration: null,
country: null,
productionDate: null,
season: null,
episode: null
}
}
// Check last part: date if numeric
const dateCandidate = parts[len - 1]
const productionDate = /^\d{4}$/.test(dateCandidate) ? dateCandidate : null
// Check for duration (first part containing "minutes")
let durationMinute = null
//let duration = null
let episode = null
let season = null
let durationIndex = -1
for (let i = 0; i < len; i++) {
if (parts[i].toLowerCase().includes('minute')) {
durationMinute = parts[i].trim()
durationMinute = durationMinute.replace('minutes', '')
durationMinute = durationMinute.replace('minute', '')
//duration = [{ units: 'minutes', value: durationMinute }],
durationIndex = i
} else if (parts[i].toLowerCase().includes('épisode')) {
const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i)
if (match) {
episode = parseInt(match[1], 10)
}
} else if (parts[i].toLowerCase().includes('saison')) {
season = parts[i].replace('saison', '').trim()
}
}
// Country: second to last
const countryIndex = len - 2
let country = durationIndex === countryIndex ? null : parts[countryIndex]
return {
category,
durationMinute,
country,
productionDate,
season,
episode
}
}
function parseTitle($item) {
return $item('.channel-programs-title a').text().trim()
}
function parseDescription($item) {
return $item('#descr').text().trim() || null
}
function parseDescriptionURL($item) {
const descrLink = $item('#descr a')
return descrLink.attr('href') || null
}
function parseCategory($item) {
let type = null
$item('.channel-programs-title span').each((i, span) => {
const className = $item(span).attr('class')
if (className && className.startsWith('text_bg')) {
type = $item(span).text().trim()
}
})
return type
}
function parseStart($item, itemDate) {
const dt = $item('.channel-programs-time a').text().trim()
if (!dt) return null
const datetimeStr = `${itemDate} ${dt}`
return dayjs.tz(datetimeStr, 'YYYY-MM-DD HH:mm', PARIS_TZ)
}
function parseItems(content) {
const $ = cheerio.load(content)
// Extract header information
const logoSrc = $('#logo img').attr('src')
const title = $('#title h1').text().trim()
const subtitle = $('#subtitle').text().trim()
const dateMatch = subtitle.match(/(\d{1,2} \w+ \d{4})/)
const dateStr = dateMatch ? dateMatch[1].toLowerCase() : null
// Parse the French date string
const parsedDate = dayjs(dateStr, 'D MMMM YYYY', 'fr')
// Format it as YYYY-MM-DD
const formattedDate = parsedDate.format('YYYY-MM-DD')
const rows = $('.channel-row').toArray()
return {
rows,
logoSrc,
title,
formattedDate
}
}

View File

@@ -1,85 +1,85 @@
const { parser, url } = require('./guidetnt.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')
const timezone = require('dayjs/plugin/timezone')
require('dayjs/locale/fr')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'tf1',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1')
})
it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
let results = await parser({ content, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(29)
expect(results[0]).toMatchObject({
category: 'Série',
description:
"Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...",
start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis'
})
expect(results[2]).toMatchObject({
category: 'Magazine',
description: 'Retrouvez tous vos programmes de nuit.',
start: '2025-07-01T00:55:00.000Z',
stop: '2025-07-01T04:00:00.000Z',
title: 'Programmes de la nuit'
})
expect(results[15]).toMatchObject({
category: 'Téléfilm',
description:
"La vie quasi parfaite de Riley bascule brutalement lorsqu'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l'enquête débute, l'affaire prend une tournure étrange l...",
start: '2025-07-01T12:25:00.000Z',
stop: '2025-07-01T14:00:00.000Z',
title: "Trahie par l'amour"
})
})
it('can parse response for current day', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(29)
expect(results[0]).toMatchObject({
category: 'Série',
description:
"Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...",
start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis'
})
})
it('can handle empty guide', async () => {
const results = await parser({
date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
})
expect(results).toEqual([])
})
const { parser, url } = require('./guidetnt.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')
const timezone = require('dayjs/plugin/timezone')
require('dayjs/locale/fr')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'tf1',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1')
})
it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
let results = await parser({ content, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(29)
expect(results[0]).toMatchObject({
category: 'Série',
description:
"Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...",
start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis'
})
expect(results[2]).toMatchObject({
category: 'Magazine',
description: 'Retrouvez tous vos programmes de nuit.',
start: '2025-07-01T00:55:00.000Z',
stop: '2025-07-01T04:00:00.000Z',
title: 'Programmes de la nuit'
})
expect(results[15]).toMatchObject({
category: 'Téléfilm',
description:
"La vie quasi parfaite de Riley bascule brutalement lorsqu'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l'enquête débute, l'affaire prend une tournure étrange l...",
start: '2025-07-01T12:25:00.000Z',
stop: '2025-07-01T14:00:00.000Z',
title: "Trahie par l'amour"
})
})
it('can parse response for current day', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(29)
expect(results[0]).toMatchObject({
category: 'Série',
description:
"Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...",
start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis'
})
})
it('can handle empty guide', async () => {
const results = await parser({
date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
})
expect(results).toEqual([])
})

View File

@@ -1,246 +1,246 @@
const { parser, url } = require('./horizon.tv.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-02-07', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '10024',
xmltv_id: 'AMCCzechRepublic.cz'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1'
)
})
it('can parse response', done => {
const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8'))
axios.get.mockImplementation(url => {
if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_3.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_1.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_2.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_3.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_4.json'), 'utf8'))
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content, channel, date })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-02-06T21:35:00.000Z',
stop: '2023-02-06T23:05:00.000Z',
title: 'Avengement',
description:
'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.',
category: ['Drama', 'Akcia'],
directors: ['Jesse V. Johnson'],
actors: [
'Scott Adkins',
'Craig Fairbrass',
'Thomas Turgoose',
'Nick Moran',
'Kierston Wareing',
'Leo Gregory',
'Mark Strange',
'Luke LaFontaine',
'Beau Fowler',
'Dan Styles',
'Christopher Sciueref',
'Matt Routledge',
'Jane Thorne',
'Louis Mandylor',
'Terence Maynard',
'Greg Burridge',
'Michael Higgs',
'Damian Gallagher',
'Daniel Adegboyega',
'John Ioannou',
'Sofie Golding-Spittle',
'Joe Egan',
'Darren Swain',
'Lee Charles',
'Dominic Kinnaird',
"Ross O'Hennessy",
'Teresa Mahoney',
'Andrew Dunkelberger',
'Sam Hardy',
'Ivan Moy',
'Mark Sears',
'Phillip Ray Tommy'
],
date: '2019'
},
{
start: '2023-02-07T04:35:00.000Z',
stop: '2023-02-07T05:00:00.000Z',
title: 'Zoom In',
description: 'Film/Kino',
category: ['Hudba a umenie', 'Film'],
date: '2010'
},
{
start: '2023-02-07T09:10:00.000Z',
stop: '2023-02-07T11:00:00.000Z',
title: 'Studentka',
description:
'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?',
category: ['Film', 'Komédia'],
actors: [
'Sophie Marceauová',
'Vincent Lindon',
'Elisabeth Vitali',
'Elena Pompei',
'Jean-Claude Leguay',
'Brigitte Chamarande',
'Christian Pereira',
'Gérard Dacier',
'Roberto Attias',
'Beppe Chierici',
'Nathalie Mann',
'Anne Macina',
'Janine Souchon',
'Virginie Demians',
'Hugues Leforestier',
'Jacqueline Noëlle',
'Marc-André Brunet',
'Isabelle Caubère',
'André Chazel',
'Med Salah Cheurfi',
'Guillaume Corea',
'Eric Denize',
'Gilles Gaston-Dreyfuss',
'Benoît Gourley',
'Marc Innocenti',
'Najim Laouriga',
'Laurent Ledermann',
'Philippe Maygal',
'Dominique Pifarely',
'Ysé Tran'
],
directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'],
date: '1988'
},
{
start: '2023-02-07T16:05:00.000Z',
stop: '2023-02-07T17:45:00.000Z',
title: 'Zilionáři',
description:
'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...',
category: ['Drama', 'Akcia'],
actors: [
'Zach Galifianakis',
'Kristen Wiigová',
'Owen Wilson',
'Kate McKinnon',
'Leslie Jones',
'Jason Sudeikis',
'Ross Kimball',
'Devin Ratray',
'Mary Elizabeth Ellisová',
'Jon Daly',
'Ken Marino',
'Daniel Zacapa',
'Tom Werme',
'Njema Williams',
'Nils Cruz',
'Michael Fraguada',
'Christian Gonzalez',
'Candace Blanchard',
'Karsten Friske',
'Dallas Edwards',
'Barry Ratcliffe',
'Shelton Grant',
'Laura Palka',
'Reegus Flenory',
'Wynn Reichert',
'Jill Jane Clements',
'Joseph S. Wilson',
'Jee An',
'Rhoda Griffisová',
'Nicole Dupre Sobchack'
],
directors: [
'Scott August',
'Richard L. Fox',
'Michelle Malley-Campos',
'Sebastian Mazzola',
'Steven Ritzi',
'Pete Waterman',
'Jared Hess'
],
date: '2016'
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
channel,
date
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})
const { parser, url } = require('./horizon.tv.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-02-07', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '10024',
xmltv_id: 'AMCCzechRepublic.cz'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1'
)
})
it('can parse response', done => {
const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8'))
axios.get.mockImplementation(url => {
if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_3.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_1.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_2.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_3.json'), 'utf8'))
})
} else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_4.json'), 'utf8'))
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content, channel, date })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-02-06T21:35:00.000Z',
stop: '2023-02-06T23:05:00.000Z',
title: 'Avengement',
description:
'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.',
category: ['Drama', 'Akcia'],
directors: ['Jesse V. Johnson'],
actors: [
'Scott Adkins',
'Craig Fairbrass',
'Thomas Turgoose',
'Nick Moran',
'Kierston Wareing',
'Leo Gregory',
'Mark Strange',
'Luke LaFontaine',
'Beau Fowler',
'Dan Styles',
'Christopher Sciueref',
'Matt Routledge',
'Jane Thorne',
'Louis Mandylor',
'Terence Maynard',
'Greg Burridge',
'Michael Higgs',
'Damian Gallagher',
'Daniel Adegboyega',
'John Ioannou',
'Sofie Golding-Spittle',
'Joe Egan',
'Darren Swain',
'Lee Charles',
'Dominic Kinnaird',
"Ross O'Hennessy",
'Teresa Mahoney',
'Andrew Dunkelberger',
'Sam Hardy',
'Ivan Moy',
'Mark Sears',
'Phillip Ray Tommy'
],
date: '2019'
},
{
start: '2023-02-07T04:35:00.000Z',
stop: '2023-02-07T05:00:00.000Z',
title: 'Zoom In',
description: 'Film/Kino',
category: ['Hudba a umenie', 'Film'],
date: '2010'
},
{
start: '2023-02-07T09:10:00.000Z',
stop: '2023-02-07T11:00:00.000Z',
title: 'Studentka',
description:
'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?',
category: ['Film', 'Komédia'],
actors: [
'Sophie Marceauová',
'Vincent Lindon',
'Elisabeth Vitali',
'Elena Pompei',
'Jean-Claude Leguay',
'Brigitte Chamarande',
'Christian Pereira',
'Gérard Dacier',
'Roberto Attias',
'Beppe Chierici',
'Nathalie Mann',
'Anne Macina',
'Janine Souchon',
'Virginie Demians',
'Hugues Leforestier',
'Jacqueline Noëlle',
'Marc-André Brunet',
'Isabelle Caubère',
'André Chazel',
'Med Salah Cheurfi',
'Guillaume Corea',
'Eric Denize',
'Gilles Gaston-Dreyfuss',
'Benoît Gourley',
'Marc Innocenti',
'Najim Laouriga',
'Laurent Ledermann',
'Philippe Maygal',
'Dominique Pifarely',
'Ysé Tran'
],
directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'],
date: '1988'
},
{
start: '2023-02-07T16:05:00.000Z',
stop: '2023-02-07T17:45:00.000Z',
title: 'Zilionáři',
description:
'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...',
category: ['Drama', 'Akcia'],
actors: [
'Zach Galifianakis',
'Kristen Wiigová',
'Owen Wilson',
'Kate McKinnon',
'Leslie Jones',
'Jason Sudeikis',
'Ross Kimball',
'Devin Ratray',
'Mary Elizabeth Ellisová',
'Jon Daly',
'Ken Marino',
'Daniel Zacapa',
'Tom Werme',
'Njema Williams',
'Nils Cruz',
'Michael Fraguada',
'Christian Gonzalez',
'Candace Blanchard',
'Karsten Friske',
'Dallas Edwards',
'Barry Ratcliffe',
'Shelton Grant',
'Laura Palka',
'Reegus Flenory',
'Wynn Reichert',
'Jill Jane Clements',
'Joseph S. Wilson',
'Jee An',
'Rhoda Griffisová',
'Nicole Dupre Sobchack'
],
directors: [
'Scott August',
'Richard L. Fox',
'Michelle Malley-Campos',
'Sebastian Mazzola',
'Steven Ritzi',
'Pete Waterman',
'Jared Hess'
],
date: '2016'
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
channel,
date
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./hoy.tv.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '76',
xmltv_id: 'HOYIBC.hk',
lang: 'zh'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'), 'utf8')
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2024-09-13T03:30:00.000Z',
stop: '2024-09-13T04:30:00.000Z',
title: '點講都係一家人[PG]',
sub_title: '第46集'
},
{
start: '2024-09-13T04:30:00.000Z',
stop: '2024-09-13T05:30:00.000Z',
title: '麝香之路',
description:
'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world'
}
])
})
const { parser, url } = require('./hoy.tv.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '76',
xmltv_id: 'HOYIBC.hk',
lang: 'zh'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'), 'utf8')
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2024-09-13T03:30:00.000Z',
stop: '2024-09-13T04:30:00.000Z',
title: '點講都係一家人[PG]',
sub_title: '第46集'
},
{
start: '2024-09-13T04:30:00.000Z',
stop: '2024-09-13T05:30:00.000Z',
title: '麝香之路',
description:
'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world'
}
])
})

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./i24news.tv.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-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ar',
xmltv_id: 'I24NewsArabic.il'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-06T13:00:00.000Z',
stop: '2022-03-06T13:28:00.000Z',
title: 'تغطية خاصة',
description: 'Special Edition',
image:
'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]',
date
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./i24news.tv.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-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ar',
xmltv_id: 'I24NewsArabic.il'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-06T13:00:00.000Z',
stop: '2022-03-06T13:28:00.000Z',
title: 'تغطية خاصة',
description: 'Special Edition',
image:
'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]',
date
})
expect(result).toMatchObject([])
})

View File

@@ -1,57 +1,57 @@
const { parser, url } = require('./indihometv.com.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('2022-08-08').startOf('d')
const channel = {
site_id: 'metrotv',
xmltv_id: 'MetroTV.id'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.indihometv.com/livetv/metrotv')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'Headline News',
start: '2022-08-08T00:00:00.000Z',
stop: '2022-08-08T00:05:00.000Z'
},
{
title: 'Editorial Media Indonesia',
start: '2022-08-08T00:05:00.000Z',
stop: '2022-08-08T00:30:00.000Z'
},
{
title: 'Editorial Media Indonesia',
start: '2022-08-08T00:30:00.000Z',
stop: '2022-08-08T00:45:00.000Z'
},
{
title: 'Editorial Media Indonesia',
start: '2022-08-08T00:45:00.000Z',
stop: '2022-08-08T01:00:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./indihometv.com.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('2022-08-08').startOf('d')
const channel = {
site_id: 'metrotv',
xmltv_id: 'MetroTV.id'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.indihometv.com/livetv/metrotv')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'Headline News',
start: '2022-08-08T00:00:00.000Z',
stop: '2022-08-08T00:05:00.000Z'
},
{
title: 'Editorial Media Indonesia',
start: '2022-08-08T00:05:00.000Z',
stop: '2022-08-08T00:30:00.000Z'
},
{
title: 'Editorial Media Indonesia',
start: '2022-08-08T00:30:00.000Z',
stop: '2022-08-08T00:45:00.000Z'
},
{
title: 'Editorial Media Indonesia',
start: '2022-08-08T00:45:00.000Z',
stop: '2022-08-08T01:00:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,54 +1,54 @@
const { parser, url } = require('./ipko.tv.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-12-24', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'ipko-promo',
xmltv_id: 'IPKOPROMO'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel })
expect(result).toMatchObject([
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T04:00:00.000Z',
stop: '2024-12-24T06:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
},
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T06:00:00.000Z',
stop: '2024-12-24T08:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
},
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T08:00:00.000Z',
stop: '2024-12-24T10:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./ipko.tv.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-12-24', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'ipko-promo',
xmltv_id: 'IPKOPROMO'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel })
expect(result).toMatchObject([
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T04:00:00.000Z',
stop: '2024-12-24T06:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
},
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T06:00:00.000Z',
stop: '2024-12-24T08:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
},
{
title: 'IPKO Promo',
description: 'No description available',
start: '2024-12-24T08:00:00.000Z',
stop: '2024-12-24T10:00:00.000Z',
thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,47 +1,47 @@
const { parser, url } = require('./kan.org.il.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-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '19',
xmltv_id: 'KANEducational.il'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-05T22:05:37.000Z',
stop: '2022-03-05T22:27:12.000Z',
title: 'ארץ מולדת - בין תורכיה לבריטניה',
description:
"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.",
image: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./kan.org.il.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-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '19',
xmltv_id: 'KANEducational.il'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-05T22:05:37.000Z',
stop: '2022-03-05T22:27:12.000Z',
title: 'ארץ מולדת - בין תורכיה לבריטניה',
description:
"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.",
image: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})

View File

@@ -1,64 +1,64 @@
const { parser, url, request } = require('./magticom.ge.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('2021-11-22', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '260',
xmltv_id: 'BollywoodHDRussia.ru'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.magticom.ge/request/channel-program.php')
})
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',
Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide'
})
})
it('can generate valid request data', () => {
const result = request.data({ channel, date })
expect(result.has('channelId')).toBe(true)
expect(result.has('start')).toBe(true)
expect(result.has('end')).toBe(true)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-22T03:00:00.000Z',
stop: '2021-11-22T05:00:00.000Z',
title: 'Х/ф "Неравный брак".',
description:
'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '[]'
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./magticom.ge.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('2021-11-22', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '260',
xmltv_id: 'BollywoodHDRussia.ru'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.magticom.ge/request/channel-program.php')
})
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',
Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide'
})
})
it('can generate valid request data', () => {
const result = request.data({ channel, date })
expect(result.has('channelId')).toBe(true)
expect(result.has('start')).toBe(true)
expect(result.has('end')).toBe(true)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-22T03:00:00.000Z',
stop: '2021-11-22T05:00:00.000Z',
title: 'Х/ф "Неравный брак".',
description:
'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '[]'
})
expect(result).toMatchObject([])
})

View File

@@ -1,41 +1,41 @@
const { parser, url } = require('./mako.co.il.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-03-07', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => {
expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-07T00:38:00.000Z',
stop: '2022-03-07T00:39:00.000Z',
title: 'רוקדים עם כוכבים - בר זומר',
description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.',
image: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]',
date
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./mako.co.il.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-03-07', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => {
expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-07T00:38:00.000Z',
stop: '2022-03-07T00:39:00.000Z',
title: 'רוקדים עם כוכבים - בר זומר',
description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.',
image: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]',
date
})
expect(result).toMatchObject([])
})

View File

@@ -1,72 +1,72 @@
const { parser, url } = require('./maxtvgo.mk.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '105',
xmltv_id: 'MRT1.mk'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-16T22:10:00.000Z',
stop: '2021-11-17T00:00:00.000Z',
title: 'Палмето - игран филм',
category: 'Останато',
description:
'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.',
image:
'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1'
}
])
})
it('can parse response with no description', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_no_description.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-16T22:10:00.000Z',
stop: '2021-11-17T00:00:00.000Z',
title: 'Палмето - игран филм',
category: 'Останато',
description: null,
image:
'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./maxtvgo.mk.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '105',
xmltv_id: 'MRT1.mk'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-16T22:10:00.000Z',
stop: '2021-11-17T00:00:00.000Z',
title: 'Палмето - игран филм',
category: 'Останато',
description:
'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.',
image:
'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1'
}
])
})
it('can parse response with no description', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_no_description.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-16T22:10:00.000Z',
stop: '2021-11-17T00:00:00.000Z',
title: 'Палмето - игран филм',
category: 'Останато',
description: null,
image:
'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./melita.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-04-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995',
xmltv_id: 'TVM.mt'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-04-20T06:25:00.000Z',
stop: '2022-04-20T06:45:00.000Z',
title: 'How I Met Your Mother',
description:
'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.',
season: 7,
episode: 12,
image:
'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6',
category: ['comedy']
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '{}'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./melita.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-04-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995',
xmltv_id: 'TVM.mt'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-04-20T06:25:00.000Z',
stop: '2022-04-20T06:45:00.000Z',
title: 'How I Met Your Mother',
description:
'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.',
season: 7,
episode: 12,
image:
'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6',
category: ['comedy']
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '{}'
})
expect(result).toMatchObject([])
})

View File

@@ -1,56 +1,56 @@
const { parser, url } = require('./mewatch.sg.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-06-11', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '97098',
xmltv_id: 'Channel5Singapore.sg'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-10&duration=24&ff=idp,ldp,rpt,cd&hour=12&intersect=true&lang=en&segments=all'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-06-11T21:00:00.000Z',
stop: '2022-06-11T21:30:00.000Z',
title: 'Open Homes S3 - EP 2',
description:
'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.',
image:
"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all",
episode: 2,
season: 3,
rating: {
system: 'IMDA',
value: 'G'
}
}
])
})
it('can handle empty guide', () => {
const result = parser({
content:
fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
channel
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./mewatch.sg.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-06-11', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '97098',
xmltv_id: 'Channel5Singapore.sg'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-10&duration=24&ff=idp,ldp,rpt,cd&hour=12&intersect=true&lang=en&segments=all'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-06-11T21:00:00.000Z',
stop: '2022-06-11T21:30:00.000Z',
title: 'Open Homes S3 - EP 2',
description:
'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.',
image:
"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all",
episode: 2,
season: 3,
rating: {
system: 'IMDA',
value: 'G'
}
}
])
})
it('can handle empty guide', () => {
const result = parser({
content:
fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
channel
})
expect(result).toMatchObject([])
})

View File

@@ -1,144 +1,144 @@
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
const headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'accept-language': 'en',
'sec-fetch-site': 'same-origin',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
}
module.exports = {
site: 'mi.tv',
days: 2,
request: { headers },
url({ date, channel }) {
const [country, id] = channel.site_id.split('#')
return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0`
},
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) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(1, 'h')
programs.push({
title: parseTitle($item),
category: parseCategory($item),
description: parseDescription($item),
image: parseImage($item),
start,
stop
})
})
return programs
},
async channels({ country }) {
let lang = 'es'
if (country === 'br') lang = 'pt'
const axios = require('axios')
const data = await axios
.get(`https://mi.tv/${country}/sitemap`)
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
let channels = []
$(`#page-contents a[href*="${country}/canales"], a[href*="${country}/canais"]`).each(
(i, el) => {
const name = $(el).text()
const url = $(el).attr('href')
const [, , , channelId] = url.split('/')
channels.push({
lang,
name,
site_id: `${country}#${channelId}`
})
}
)
return channels
}
}
function parseStart($item, date) {
const timeString = $item('a > div.content > span.time').text()
if (!timeString) return null
const dateString = `${date.format('MM/DD/YYYY')} ${timeString}`
return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm')
}
function parseTitle($item) {
return $item('a > div.content > h2').text().trim()
}
function parseCategory($item) {
return $item('a > div.content > span.sub-title').text().trim()
}
function parseDescription($item) {
return $item('a > div.content > p.synopsis').text().trim()
}
function parseImage($item) {
const styleAttr = $item('a > div.image-parent > div.image').attr('style')
if (styleAttr) {
const match = styleAttr.match(/background-image:\s*url\(['"]?(.*?)['"]?\)/)
if (match) {
return cleanUrl(match[1])
}
}
const backgroundImage = $item('a > div.image-parent > div.image').css('background-image')
if (backgroundImage && backgroundImage !== 'none') {
const match = backgroundImage.match(/url\(['"]?(.*?)['"]?\)/)
if (match) {
return cleanUrl(match[1])
}
}
return null
}
function cleanUrl(url) {
if (!url) return null
return url
.replace(/^['"`\\]+/, '')
.replace(/['"`\\]+$/, '')
.replace(/\\'/g, "'")
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#listings > ul > li').toArray()
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
const headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'accept-language': 'en',
'sec-fetch-site': 'same-origin',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
}
module.exports = {
site: 'mi.tv',
days: 2,
request: { headers },
url({ date, channel }) {
const [country, id] = channel.site_id.split('#')
return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0`
},
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) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(1, 'h')
programs.push({
title: parseTitle($item),
category: parseCategory($item),
description: parseDescription($item),
image: parseImage($item),
start,
stop
})
})
return programs
},
async channels({ country }) {
let lang = 'es'
if (country === 'br') lang = 'pt'
const axios = require('axios')
const data = await axios
.get(`https://mi.tv/${country}/sitemap`)
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
let channels = []
$(`#page-contents a[href*="${country}/canales"], a[href*="${country}/canais"]`).each(
(i, el) => {
const name = $(el).text()
const url = $(el).attr('href')
const [, , , channelId] = url.split('/')
channels.push({
lang,
name,
site_id: `${country}#${channelId}`
})
}
)
return channels
}
}
function parseStart($item, date) {
const timeString = $item('a > div.content > span.time').text()
if (!timeString) return null
const dateString = `${date.format('MM/DD/YYYY')} ${timeString}`
return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm')
}
function parseTitle($item) {
return $item('a > div.content > h2').text().trim()
}
function parseCategory($item) {
return $item('a > div.content > span.sub-title').text().trim()
}
function parseDescription($item) {
return $item('a > div.content > p.synopsis').text().trim()
}
function parseImage($item) {
const styleAttr = $item('a > div.image-parent > div.image').attr('style')
if (styleAttr) {
const match = styleAttr.match(/background-image:\s*url\(['"]?(.*?)['"]?\)/)
if (match) {
return cleanUrl(match[1])
}
}
const backgroundImage = $item('a > div.image-parent > div.image').css('background-image')
if (backgroundImage && backgroundImage !== 'none') {
const match = backgroundImage.match(/url\(['"]?(.*?)['"]?\)/)
if (match) {
return cleanUrl(match[1])
}
}
return null
}
function cleanUrl(url) {
if (!url) return null
return url
.replace(/^['"`\\]+/, '')
.replace(/['"`\\]+$/, '')
.replace(/\\'/g, "'")
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#listings > ul > li').toArray()
}

View File

@@ -1,67 +1,67 @@
const { parser, url } = require('./mi.tv.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('2021-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ar#24-7-canal-de-noticias',
xmltv_id: '247CanaldeNoticias.ar'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T03:00:00.000Z',
stop: '2021-11-24T23:00:00.000Z',
title: 'Trasnoche de 24/7',
category: 'Interés general',
description: 'Lo más visto de la semana en nuestra pantalla.',
image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
},
{
start: '2021-11-24T23:00:00.000Z',
stop: '2021-11-25T01:00:00.000Z',
title: 'Noticiero central - Segunda edición',
category: 'Noticiero',
description:
'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.',
image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
},
{
start: '2021-11-25T01:00:00.000Z',
stop: '2021-11-25T02:00:00.000Z',
title: 'Plus energético',
category: 'Cultural',
description:
'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.',
image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: ''
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./mi.tv.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('2021-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ar#24-7-canal-de-noticias',
xmltv_id: '247CanaldeNoticias.ar'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T03:00:00.000Z',
stop: '2021-11-24T23:00:00.000Z',
title: 'Trasnoche de 24/7',
category: 'Interés general',
description: 'Lo más visto de la semana en nuestra pantalla.',
image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
},
{
start: '2021-11-24T23:00:00.000Z',
stop: '2021-11-25T01:00:00.000Z',
title: 'Noticiero central - Segunda edición',
category: 'Noticiero',
description:
'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.',
image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
},
{
start: '2021-11-25T01:00:00.000Z',
stop: '2021-11-25T02:00:00.000Z',
title: 'Plus energético',
category: 'Cultural',
description:
'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.',
image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: ''
})
expect(result).toMatchObject([])
})

View File

@@ -1,29 +1,29 @@
const { parser } = require('./moji.id.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('2023-08-18', 'YYYY-MM-DD').startOf('d')
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const results = parser({ content, date }).map(p => {
p.start = p.start.year(2023).toJSON()
p.stop = p.stop.year(2023).toJSON()
return p
})
expect(results[0]).toMatchObject({
title: 'TRUST',
start: '2023-08-17T17:00:00.000Z',
stop: '2023-08-17T17:30:00.000Z',
description: 'Informasi seputar menjaga vitalitas pria'
})
})
const { parser } = require('./moji.id.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('2023-08-18', 'YYYY-MM-DD').startOf('d')
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const results = parser({ content, date }).map(p => {
p.start = p.start.year(2023).toJSON()
p.stop = p.stop.year(2023).toJSON()
return p
})
expect(results[0]).toMatchObject({
title: 'TRUST',
start: '2023-08-17T17:00:00.000Z',
stop: '2023-08-17T17:30:00.000Z',
description: 'Informasi seputar menjaga vitalitas pria'
})
})

View File

@@ -1,192 +1,192 @@
const doFetch = require('@ntlab/sfetch')
const axios = require('axios')
const dayjs = require('dayjs')
const crypto = require('crypto')
const { sortBy } = require('../../scripts/functions')
// API Configuration Constants
const NATCO_CODE = 'hr'
const APP_LANGUAGE = 'hr'
const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5'
const APP_VERSION = '02.0.1080'
const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE'
const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr'
// Role Types
const ROLE_TYPES = {
ACTOR: 'GLUMI', // Croatian for "ACTS"
DIRECTOR: 'REŽIJA', // Croatian for "DIRECTOR"
PRODUCER: 'PRODUKCIJA', // Croatian for "PRODUCER"
WRITER: 'AUTOR',
SCENARIO: 'SCENARIJ'
}
// Dynamic API Endpoint based on NATCO_CODE
const API_ENDPOINT = `https://tv-${NATCO_CODE}-prod.yo-digital.com/${NATCO_CODE}-bifrost`
// Session/Device IDs
const DEVICE_ID = crypto.randomUUID()
const SESSION_ID = crypto.randomUUID()
const cached = {}
const getHeaders = () => ({
'app_key': APP_KEY,
'app_version': APP_VERSION,
'device-id': DEVICE_ID,
'tenant': 'tv',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
'origin': `https://${SITE_URL}`,
'x-request-session-id': SESSION_ID,
'x-request-tracking-id': crypto.randomUUID(),
'x-tv-step': 'EPG_SCHEDULES',
'x-tv-flow': 'EPG',
'x-call-type': 'GUEST_USER',
'x-user-agent': `web|web|Chrome-133|${APP_VERSION}|1`
})
module.exports = {
site: SITE_URL,
url({ date }) {
return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format(
'YYYY-MM-DD'
)}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`
},
request: {
headers: getHeaders(),
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
async parser({ content, channel, date }) {
const data = parseData(content)
if (!data) return []
let items = parseItems(data, channel)
if (!items.length) return []
const queue = [3, 6, 9, 12, 15, 18, 21]
.map(offset => {
const url = module.exports.url({ date }).replace('hour_offset=0', `hour_offset=${offset}`)
const params = { ...module.exports.request, headers: getHeaders() }
if (cached[url]) {
items = items.concat(parseItems(cached[url], channel))
return null
}
return { url, params }
})
.filter(Boolean)
await doFetch(queue, (_req, _data) => {
if (_data) {
cached[_req.url] = _data
items = items.concat(parseItems(_data, channel))
}
})
items = sortBy(items, i => dayjs(i.start_time).valueOf())
// Fetch program details for each item
const programs = []
for (let item of items) {
const detail = await loadProgramDetails(item)
// detectUnknownRoles(detail)
programs.push({
title: item.description,
sub_title: item.episode_name,
description: parseDescription(detail),
categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [],
date: parseDate(item),
image: detail.poster_image_url,
actors: parseRoles(detail, ROLE_TYPES.ACTOR),
directors: parseRoles(detail, ROLE_TYPES.DIRECTOR),
producers: parseRoles(detail, ROLE_TYPES.PRODUCER),
season: parseSeason(item),
episode: parseEpisode(item),
rating: parseRating(item),
start: item.start_time,
stop: item.end_time
})
}
return programs
},
async channels() {
const data = await axios
.get(
`${API_ENDPOINT}/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=${NATCO_KEY}&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`,
{ ...module.exports.request, headers: getHeaders() }
)
.then(r => r.data)
.catch(console.error)
return data.channels.map(channel => ({
lang: NATCO_CODE,
name: channel.title,
site_id: channel.station_id
}))
}
}
async function loadProgramDetails(item) {
if (!item.program_id) return {}
const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}`
const data = await axios
.get(url, { headers: getHeaders() })
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseData(content) {
try {
const data = JSON.parse(content)
return data || null
} catch {
return null
}
}
function parseItems(data, channel) {
if (!data.channels || !Array.isArray(data.channels[channel.site_id])) return []
return data.channels[channel.site_id]
}
function parseDate(item) {
return item && item.release_year ? item.release_year.toString() : null
}
function parseRating(item) {
return item.ratings
? {
system: 'MPA',
value: item.ratings
}
: null
}
function parseSeason(item) {
if (item.season_display_number === 'Epizode') return null // 'Epizode' is 'Episodes' in Croatian
return item.season_number
}
function parseEpisode(item) {
if (item.episode_number) return parseInt(item.episode_number)
if (item.season_display_number === 'Epizode') return item.season_number
return null
}
function parseDescription(item) {
if (!item.details) return null
return item.details.description
}
function parseRoles(item, role_name) {
if (!item.roles) return null
return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name)
}
const doFetch = require('@ntlab/sfetch')
const axios = require('axios')
const dayjs = require('dayjs')
const crypto = require('crypto')
const { sortBy } = require('../../scripts/functions')
// API Configuration Constants
const NATCO_CODE = 'hr'
const APP_LANGUAGE = 'hr'
const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5'
const APP_VERSION = '02.0.1080'
const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE'
const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr'
// Role Types
const ROLE_TYPES = {
ACTOR: 'GLUMI', // Croatian for "ACTS"
DIRECTOR: 'REŽIJA', // Croatian for "DIRECTOR"
PRODUCER: 'PRODUKCIJA', // Croatian for "PRODUCER"
WRITER: 'AUTOR',
SCENARIO: 'SCENARIJ'
}
// Dynamic API Endpoint based on NATCO_CODE
const API_ENDPOINT = `https://tv-${NATCO_CODE}-prod.yo-digital.com/${NATCO_CODE}-bifrost`
// Session/Device IDs
const DEVICE_ID = crypto.randomUUID()
const SESSION_ID = crypto.randomUUID()
const cached = {}
const getHeaders = () => ({
'app_key': APP_KEY,
'app_version': APP_VERSION,
'device-id': DEVICE_ID,
'tenant': 'tv',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
'origin': `https://${SITE_URL}`,
'x-request-session-id': SESSION_ID,
'x-request-tracking-id': crypto.randomUUID(),
'x-tv-step': 'EPG_SCHEDULES',
'x-tv-flow': 'EPG',
'x-call-type': 'GUEST_USER',
'x-user-agent': `web|web|Chrome-133|${APP_VERSION}|1`
})
module.exports = {
site: SITE_URL,
url({ date }) {
return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format(
'YYYY-MM-DD'
)}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`
},
request: {
headers: getHeaders(),
cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
async parser({ content, channel, date }) {
const data = parseData(content)
if (!data) return []
let items = parseItems(data, channel)
if (!items.length) return []
const queue = [3, 6, 9, 12, 15, 18, 21]
.map(offset => {
const url = module.exports.url({ date }).replace('hour_offset=0', `hour_offset=${offset}`)
const params = { ...module.exports.request, headers: getHeaders() }
if (cached[url]) {
items = items.concat(parseItems(cached[url], channel))
return null
}
return { url, params }
})
.filter(Boolean)
await doFetch(queue, (_req, _data) => {
if (_data) {
cached[_req.url] = _data
items = items.concat(parseItems(_data, channel))
}
})
items = sortBy(items, i => dayjs(i.start_time).valueOf())
// Fetch program details for each item
const programs = []
for (let item of items) {
const detail = await loadProgramDetails(item)
// detectUnknownRoles(detail)
programs.push({
title: item.description,
sub_title: item.episode_name,
description: parseDescription(detail),
categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [],
date: parseDate(item),
image: detail.poster_image_url,
actors: parseRoles(detail, ROLE_TYPES.ACTOR),
directors: parseRoles(detail, ROLE_TYPES.DIRECTOR),
producers: parseRoles(detail, ROLE_TYPES.PRODUCER),
season: parseSeason(item),
episode: parseEpisode(item),
rating: parseRating(item),
start: item.start_time,
stop: item.end_time
})
}
return programs
},
async channels() {
const data = await axios
.get(
`${API_ENDPOINT}/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=${NATCO_KEY}&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`,
{ ...module.exports.request, headers: getHeaders() }
)
.then(r => r.data)
.catch(console.error)
return data.channels.map(channel => ({
lang: NATCO_CODE,
name: channel.title,
site_id: channel.station_id
}))
}
}
async function loadProgramDetails(item) {
if (!item.program_id) return {}
const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}`
const data = await axios
.get(url, { headers: getHeaders() })
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseData(content) {
try {
const data = JSON.parse(content)
return data || null
} catch {
return null
}
}
function parseItems(data, channel) {
if (!data.channels || !Array.isArray(data.channels[channel.site_id])) return []
return data.channels[channel.site_id]
}
function parseDate(item) {
return item && item.release_year ? item.release_year.toString() : null
}
function parseRating(item) {
return item.ratings
? {
system: 'MPA',
value: item.ratings
}
: null
}
function parseSeason(item) {
if (item.season_display_number === 'Epizode') return null // 'Epizode' is 'Episodes' in Croatian
return item.season_number
}
function parseEpisode(item) {
if (item.episode_number) return parseInt(item.episode_number)
if (item.season_display_number === 'Epizode') return item.season_number
return null
}
function parseDescription(item) {
if (!item.details) return null
return item.details.description
}
function parseRoles(item, role_name) {
if (!item.roles) return null
return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name)
}

View File

@@ -1,110 +1,110 @@
const doFetch = require('@ntlab/sfetch')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const { sortBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'mtel.ba',
days: 2,
url({ channel, date }) {
const [platform] = channel.site_id.split('#')
return `https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-${platform}&pageSize=999&date=${date.format(
'YYYY-MM-DD'
)}`
},
request: {
timeout: 20000, // 20 seconds
maxContentLength: 10000000, // 10 Mb
cache: {
interpretHeader: false,
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
programs.push({
title: item.title,
description: item.description,
categories: parseCategories(item),
image: parseImage(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
},
async channels({ platform = 'msat' }) {
const platforms = {
msat: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-msat',
iptv: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-iptv'
}
const queue = [
{
platform,
url: platforms[platform]
}
]
let channels = []
await doFetch(queue, (req, data) => {
if (data && data.pagination.currentPage < data.pagination.totalPages) {
queue.push({
platform: req.platform,
url: platforms[req.platform]
})
}
data.products.forEach(channel => {
channels.push({
lang: 'bs',
name: channel.name,
site_id: `${req.platform}#${channel.code}`
})
})
})
return channels
}
}
function parseStart(item) {
return dayjs.tz(item.start, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo')
}
function parseStop(item) {
return dayjs.tz(item.end, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo')
}
function parseCategories(item) {
return item.category ? item.category.split(' / ') : []
}
function parseImage(item) {
return item?.picture?.url ? item.picture.url : null
}
function parseItems(content, channel) {
try {
const data = JSON.parse(content)
if (!data || !Array.isArray(data.products)) return []
const [, channelId] = channel.site_id.split('#')
const channelData = data.products.find(channel => channel.code === channelId)
if (!channelData || !Array.isArray(channelData.programs)) return []
// filter out programs that have the sentence "no program information available"
channelData.programs = channelData.programs.filter(p => !p.title.includes('Nema informacija o programu'))
return sortBy(channelData.programs, p => parseStart(p).valueOf())
} catch {
return []
}
}
const doFetch = require('@ntlab/sfetch')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const { sortBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'mtel.ba',
days: 2,
url({ channel, date }) {
const [platform] = channel.site_id.split('#')
return `https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-${platform}&pageSize=999&date=${date.format(
'YYYY-MM-DD'
)}`
},
request: {
timeout: 20000, // 20 seconds
maxContentLength: 10000000, // 10 Mb
cache: {
interpretHeader: false,
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
programs.push({
title: item.title,
description: item.description,
categories: parseCategories(item),
image: parseImage(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
},
async channels({ platform = 'msat' }) {
const platforms = {
msat: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-msat',
iptv: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-iptv'
}
const queue = [
{
platform,
url: platforms[platform]
}
]
let channels = []
await doFetch(queue, (req, data) => {
if (data && data.pagination.currentPage < data.pagination.totalPages) {
queue.push({
platform: req.platform,
url: platforms[req.platform]
})
}
data.products.forEach(channel => {
channels.push({
lang: 'bs',
name: channel.name,
site_id: `${req.platform}#${channel.code}`
})
})
})
return channels
}
}
function parseStart(item) {
return dayjs.tz(item.start, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo')
}
function parseStop(item) {
return dayjs.tz(item.end, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo')
}
function parseCategories(item) {
return item.category ? item.category.split(' / ') : []
}
function parseImage(item) {
return item?.picture?.url ? item.picture.url : null
}
function parseItems(content, channel) {
try {
const data = JSON.parse(content)
if (!data || !Array.isArray(data.products)) return []
const [, channelId] = channel.site_id.split('#')
const channelData = data.products.find(channel => channel.code === channelId)
if (!channelData || !Array.isArray(channelData.programs)) return []
// filter out programs that have the sentence "no program information available"
channelData.programs = channelData.programs.filter(p => !p.title.includes('Nema informacija o programu'))
return sortBy(channelData.programs, p => parseStart(p).valueOf())
} catch {
return []
}
}

View File

@@ -1,58 +1,58 @@
const { parser, url } = require('./mtel.ba.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-04', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'msat#ch-11-rtrs' }
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat&pageSize=999&date=2025-02-04'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ channel, content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(38)
expect(results[0]).toMatchObject({
start: '2025-02-03T22:38:00.000Z',
stop: '2025-02-03T23:38:00.000Z',
title: 'Neka pesma kaže',
image:
'https://medias.services.mtel.ba/medias/407368591.jpg?context=bWFzdGVyfHJvb3R8MTM2MTZ8aW1hZ2UvanBlZ3xhR1F5TDJnell5ODBOekExTmpFMk1qRTJNRFkzTUM4ME1EY3pOamcxT1RFdWFuQm58ZWM3Zjc4MDNlZTY5OWU1ZGJiZDI5N2UzMDg4ODA3NzQ1NWM0OThlMjdhYmU4MjI4NGJhOWE2YzYwMTc5ODM3NQ',
description:
'Zabavni-muzički program donosi nam divne zvukove prave, narodne muzike, u kojoj se izvođači oslanjaju na kvalitet i tradiciju.',
categories: ['Music', 'Ballet', 'Dance']
})
expect(results[37]).toMatchObject({
start: '2025-02-04T22:27:00.000Z',
stop: '2025-02-04T23:58:00.000Z',
title: 'Bitanga s plaže',
image:
'https://medias.services.mtel.ba/medias/117604203.jpg?context=bWFzdGVyfHJvb3R8MTY1MTZ8aW1hZ2UvanBlZ3xhRGd6TDJnek1DODBOekExTmpFMk16STNORGM0TWk4eE1UYzJNRFF5TURNdWFuQm58YmU5MjdkOTljMGE4YjIyNjg3ZmI1YWJjYWQ0ZDY5YjA0YWJiY2RlN2E0ZGVjOTdlYzM4MzI4MzYyMzFiODBlMg',
description:
'Film prati urnebesne avanture Moondoga, buntovnika i skitnicu koji svoj život živi isključivo prema vlastitim pravilima. Uz glumačke nastupe Snoop Dogga, Zaca Efrona i Isle Fisher, Bitanga s plaže osvježavajuće je originalna i subverzivna nova komedija scenarista i redatelja Harmonyja Korinea.',
categories: ['Movie', 'Drama']
})
})
it('can handle empty guide', () => {
const results = parser({
channel,
content: '{}'
})
expect(results).toMatchObject([])
})
const { parser, url } = require('./mtel.ba.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-04', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'msat#ch-11-rtrs' }
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat&pageSize=999&date=2025-02-04'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ channel, content })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(38)
expect(results[0]).toMatchObject({
start: '2025-02-03T22:38:00.000Z',
stop: '2025-02-03T23:38:00.000Z',
title: 'Neka pesma kaže',
image:
'https://medias.services.mtel.ba/medias/407368591.jpg?context=bWFzdGVyfHJvb3R8MTM2MTZ8aW1hZ2UvanBlZ3xhR1F5TDJnell5ODBOekExTmpFMk1qRTJNRFkzTUM4ME1EY3pOamcxT1RFdWFuQm58ZWM3Zjc4MDNlZTY5OWU1ZGJiZDI5N2UzMDg4ODA3NzQ1NWM0OThlMjdhYmU4MjI4NGJhOWE2YzYwMTc5ODM3NQ',
description:
'Zabavni-muzički program donosi nam divne zvukove prave, narodne muzike, u kojoj se izvođači oslanjaju na kvalitet i tradiciju.',
categories: ['Music', 'Ballet', 'Dance']
})
expect(results[37]).toMatchObject({
start: '2025-02-04T22:27:00.000Z',
stop: '2025-02-04T23:58:00.000Z',
title: 'Bitanga s plaže',
image:
'https://medias.services.mtel.ba/medias/117604203.jpg?context=bWFzdGVyfHJvb3R8MTY1MTZ8aW1hZ2UvanBlZ3xhRGd6TDJnek1DODBOekExTmpFMk16STNORGM0TWk4eE1UYzJNRFF5TURNdWFuQm58YmU5MjdkOTljMGE4YjIyNjg3ZmI1YWJjYWQ0ZDY5YjA0YWJiY2RlN2E0ZGVjOTdlYzM4MzI4MzYyMzFiODBlMg',
description:
'Film prati urnebesne avanture Moondoga, buntovnika i skitnicu koji svoj život živi isključivo prema vlastitim pravilima. Uz glumačke nastupe Snoop Dogga, Zaca Efrona i Isle Fisher, Bitanga s plaže osvježavajuće je originalna i subverzivna nova komedija scenarista i redatelja Harmonyja Korinea.',
categories: ['Movie', 'Drama']
})
})
it('can handle empty guide', () => {
const results = parser({
channel,
content: '{}'
})
expect(results).toMatchObject([])
})

View File

@@ -1,45 +1,45 @@
const { parser, url } = require('./mysky.com.ph.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-10-04', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '8',
xmltv_id: 'KapamilyaChannel.ph'
}
it('can generate valid url', () => {
expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-10-04T11:00:00.000Z',
stop: '2022-10-04T12:00:00.000Z',
title: 'TV PATROL',
description: 'Description example'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '',
channel,
date
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./mysky.com.ph.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-10-04', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '8',
xmltv_id: 'KapamilyaChannel.ph'
}
it('can generate valid url', () => {
expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-10-04T11:00:00.000Z',
stop: '2022-10-04T12:00:00.000Z',
title: 'TV PATROL',
description: 'Description example'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '',
channel,
date
})
expect(result).toMatchObject([])
})

View File

@@ -1,61 +1,61 @@
const { parser, url } = require('./neo.io.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-12-26', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'tv-slo-1',
xmltv_id: 'TVSLO1.si'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel })
expect(result).toMatchObject([
{
title: 'Napovedujemo',
description: 'Vabilo k ogledu naših oddaj.',
start: '2024-12-26T04:05:00.000Z',
stop: '2024-12-26T05:50:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg'
},
{
title: 'S0E0 - Hrabri zajčki: Prvi sneg',
description:
'Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.',
start: '2024-12-26T05:50:00.000Z',
stop: '2024-12-26T06:00:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg'
},
{
title: 'Dobro jutro',
description:
'Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.',
start: '2024-12-26T06:00:00.000Z',
stop: '2024-12-26T09:05:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./neo.io.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-12-26', 'YYYY-MM-DD').startOf('day')
const channel = {
site_id: 'tv-slo-1',
xmltv_id: 'TVSLO1.si'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel })
expect(result).toMatchObject([
{
title: 'Napovedujemo',
description: 'Vabilo k ogledu naših oddaj.',
start: '2024-12-26T04:05:00.000Z',
stop: '2024-12-26T05:50:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg'
},
{
title: 'S0E0 - Hrabri zajčki: Prvi sneg',
description:
'Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.',
start: '2024-12-26T05:50:00.000Z',
stop: '2024-12-26T06:00:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg'
},
{
title: 'Dobro jutro',
description:
'Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.',
start: '2024-12-26T06:00:00.000Z',
stop: '2024-12-26T09:05:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./novacyprus.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '614',
xmltv_id: 'NovaCinema1.gr'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-17T04:20:00.000Z',
stop: '2021-11-17T06:10:00.000Z',
title: 'Δεσμοί Αίματος',
description: 'Θρίλερ Μυστηρίου',
image:
'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./novacyprus.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('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '614',
xmltv_id: 'NovaCinema1.gr'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-17T04:20:00.000Z',
stop: '2021-11-17T06:10:00.000Z',
title: 'Δεσμοί Αίματος',
description: 'Θρίλερ Μυστηρίου',
image:
'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,58 +1,58 @@
const { parser, url, request } = require('./nowplayer.now.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 channel = {
lang: 'zh',
site_id: '096',
xmltv_id: 'ViuTVsix.hk'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2'
)
})
it('can generate valid request headers', () => {
expect(request.headers({ channel })).toMatchObject({
Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-23T18:00:00.000Z',
stop: '2021-11-24T01:00:00.000Z',
title: 'ViuTVsix Station Closing'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[[]]'
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./nowplayer.now.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 channel = {
lang: 'zh',
site_id: '096',
xmltv_id: 'ViuTVsix.hk'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2'
)
})
it('can generate valid request headers', () => {
expect(request.headers({ channel })).toMatchObject({
Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-23T18:00:00.000Z',
stop: '2021-11-24T01:00:00.000Z',
title: 'ViuTVsix Station Closing'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[[]]'
})
expect(result).toMatchObject([])
})

View File

@@ -1,178 +1,178 @@
const axios = require('axios')
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 { uniqBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'ontvtonight.com',
days: 2,
url: function ({ date, channel }) {
const [region, id] = channel.site_id.split('#')
let url = 'https://www.ontvtonight.com'
if (region && region !== 'us') url += `/${region}`
url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}`
return url
},
parser: function ({ content, date, channel }) {
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, channel)
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(1, 'h')
programs.push({
title: parseTitle($item),
description: parseDescription($item),
start,
stop
})
})
return programs
},
async channels({ country }) {
const providers = {
au: ['o', 'a'],
ca: [
'Y464014423',
'-464014503',
'-464014594',
'-464014738',
'X3153330286',
'X464014503',
'X464013696',
'X464014594',
'X464014738',
'X464014470',
'X464013514',
'X1210684931',
'T3153330286',
'T464014503',
'T1810267316',
'T1210684931'
],
us: [
'Y341768590',
'Y1693286984',
'Y8833268284',
'-341767428',
'-341769166',
'-341769884',
'-3679985536',
'-341766967',
'X4100694897',
'X341767428',
'X341768182',
'X341767434',
'X341768272',
'X341769884',
'X3679985536',
'X3679984937',
'X341764975',
'X3679985052',
'X341766967',
'K4805071612',
'K5039655414'
]
}
const regions = {
au: [
1, 2, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 17, 18, 29, 28, 27, 26, 25, 23, 22,
21, 20, 19, 24, 30, 31, 32, 33, 34, 35, 36, 39, 38, 37, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53
],
ca: [null],
us: [null]
}
const zipcodes = {
au: [null],
ca: ['M5G1P5', 'H3B1X8', 'V6Z2H7', 'T2P3E6', 'T5J2Z2', 'K1P1B1'],
us: [10199, 90052, 60607, 77201, 85026, 19104, 78284, 92199, 75260]
}
const channels = []
for (let provider of providers[country]) {
for (let zipcode of zipcodes[country]) {
for (let region of regions[country]) {
let url = 'https://www.ontvtonight.com'
if (country === 'us') url += '/guide/schedule'
else url += `/${country}/guide/schedule`
const data = await axios
.post(url, null, {
params: {
provider,
region,
zipcode,
TVperiod: 'Night',
date: dayjs().format('YYYY-MM-DD'),
st: 0,
is_mobile: 1
}
})
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
$('.channelname').each((i, el) => {
let name = $(el).find('center > a:eq(1)').text()
name = name.replace(/--/gi, '-')
const url = $(el).find('center > a:eq(1)').attr('href')
if (!url) return
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
channels.push({
lang: 'en',
name,
site_id: `${country}#${number}/${slug}`
})
})
}
}
}
return uniqBy(channels, 'site_id')
}
}
function parseStart($item, date, channel) {
const timezones = {
au: 'Australia/Sydney',
ca: 'America/Toronto',
us: 'America/New_York'
}
const [region] = channel.site_id.split('#')
const timeString = $item('td:nth-child(1) > h5').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezones[region])
}
function parseTitle($item) {
return $item('td:nth-child(2) > h5').text().trim()
}
function parseDescription($item) {
return $item('td:nth-child(2) > h6').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#content > div > div > div > table > tbody > tr').toArray()
}
const axios = require('axios')
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 { uniqBy } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'ontvtonight.com',
days: 2,
url: function ({ date, channel }) {
const [region, id] = channel.site_id.split('#')
let url = 'https://www.ontvtonight.com'
if (region && region !== 'us') url += `/${region}`
url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}`
return url
},
parser: function ({ content, date, channel }) {
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, channel)
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(1, 'h')
programs.push({
title: parseTitle($item),
description: parseDescription($item),
start,
stop
})
})
return programs
},
async channels({ country }) {
const providers = {
au: ['o', 'a'],
ca: [
'Y464014423',
'-464014503',
'-464014594',
'-464014738',
'X3153330286',
'X464014503',
'X464013696',
'X464014594',
'X464014738',
'X464014470',
'X464013514',
'X1210684931',
'T3153330286',
'T464014503',
'T1810267316',
'T1210684931'
],
us: [
'Y341768590',
'Y1693286984',
'Y8833268284',
'-341767428',
'-341769166',
'-341769884',
'-3679985536',
'-341766967',
'X4100694897',
'X341767428',
'X341768182',
'X341767434',
'X341768272',
'X341769884',
'X3679985536',
'X3679984937',
'X341764975',
'X3679985052',
'X341766967',
'K4805071612',
'K5039655414'
]
}
const regions = {
au: [
1, 2, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 17, 18, 29, 28, 27, 26, 25, 23, 22,
21, 20, 19, 24, 30, 31, 32, 33, 34, 35, 36, 39, 38, 37, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53
],
ca: [null],
us: [null]
}
const zipcodes = {
au: [null],
ca: ['M5G1P5', 'H3B1X8', 'V6Z2H7', 'T2P3E6', 'T5J2Z2', 'K1P1B1'],
us: [10199, 90052, 60607, 77201, 85026, 19104, 78284, 92199, 75260]
}
const channels = []
for (let provider of providers[country]) {
for (let zipcode of zipcodes[country]) {
for (let region of regions[country]) {
let url = 'https://www.ontvtonight.com'
if (country === 'us') url += '/guide/schedule'
else url += `/${country}/guide/schedule`
const data = await axios
.post(url, null, {
params: {
provider,
region,
zipcode,
TVperiod: 'Night',
date: dayjs().format('YYYY-MM-DD'),
st: 0,
is_mobile: 1
}
})
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
$('.channelname').each((i, el) => {
let name = $(el).find('center > a:eq(1)').text()
name = name.replace(/--/gi, '-')
const url = $(el).find('center > a:eq(1)').attr('href')
if (!url) return
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
channels.push({
lang: 'en',
name,
site_id: `${country}#${number}/${slug}`
})
})
}
}
}
return uniqBy(channels, 'site_id')
}
}
function parseStart($item, date, channel) {
const timezones = {
au: 'Australia/Sydney',
ca: 'America/Toronto',
us: 'America/New_York'
}
const [region] = channel.site_id.split('#')
const timeString = $item('td:nth-child(1) > h5').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezones[region])
}
function parseTitle($item) {
return $item('td:nth-child(2) > h5').text().trim()
}
function parseDescription($item) {
return $item('td:nth-child(2) > h6').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#content > div > div > div > table > tbody > tr').toArray()
}

View File

@@ -1,57 +1,57 @@
const { parser, url } = require('./ontvtonight.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('2021-11-25', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'au#1692/7two',
xmltv_id: '7two.au'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T13:10:00.000Z',
stop: '2021-11-24T13:50:00.000Z',
title: 'What A Carry On'
},
{
start: '2021-11-24T13:50:00.000Z',
stop: '2021-11-25T11:50:00.000Z',
title: 'Bones',
description: 'The Devil In The Details'
},
{
start: '2021-11-25T11:50:00.000Z',
stop: '2021-11-25T12:50:00.000Z',
title: 'Inspector Morse: The Remorseful Day'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./ontvtonight.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('2021-11-25', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'au#1692/7two',
xmltv_id: '7two.au'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T13:10:00.000Z',
stop: '2021-11-24T13:50:00.000Z',
title: 'What A Carry On'
},
{
start: '2021-11-24T13:50:00.000Z',
stop: '2021-11-25T11:50:00.000Z',
title: 'Bones',
description: 'The Devil In The Details'
},
{
start: '2021-11-25T11:50:00.000Z',
stop: '2021-11-25T12:50:00.000Z',
title: 'Inspector Morse: The Remorseful Day'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,44 +1,44 @@
const { parser, url } = require('./pbsguam.org.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('2021-11-25', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'KGTF.us'
}
it('can generate valid url', () => {
expect(url).toBe('https://pbsguam.org/calendar/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ date, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-25T08:30:00.000Z',
stop: '2021-11-25T09:00:00.000Z',
title: 'Xavier Riddle and the Secret Museum'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./pbsguam.org.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('2021-11-25', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'KGTF.us'
}
it('can generate valid url', () => {
expect(url).toBe('https://pbsguam.org/calendar/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ date, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-25T08:30:00.000Z',
stop: '2021-11-25T09:00:00.000Z',
title: 'Xavier Riddle and the Secret Museum'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,95 +1,95 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'programetv.ro',
days: 2,
url: function ({ date, channel }) {
const daysOfWeek = {
0: 'duminica',
1: 'luni',
2: 'marti',
3: 'miercuri',
4: 'joi',
5: 'vineri',
6: 'sambata'
}
const day = date.day()
return `https://www.programetv.ro/program-tv/${channel.site_id}/${daysOfWeek[day]}/`
},
parser: function ({ content }) {
let programs = []
const data = parseContent(content)
if (!data || !data.shows) return programs
const items = data.shows
items.forEach(item => {
programs.push({
title: item.title,
sub_title: item.titleOriginal,
description: item.desc || item.obs,
category: item.categories,
season: item.season || null,
episode: item.episode || null,
start: parseStart(item),
stop: parseStop(item),
url: item.url || null,
date: item.date,
rating: parseRating(item),
directors: parseDirector(item),
actors: parseActor(item),
icon: item.icon
})
})
return programs
},
async channels() {
const axios = require('axios')
const data = await axios
.get('https://www.programetv.ro/api/station/index/')
.then(r => r.data)
.catch(console.log)
return data.map(item => {
return {
lang: 'ro',
site_id: item.slug,
name: item.displayName
}
})
}
}
function parseStart(item) {
return dayjs(item.start).toJSON()
}
function parseStop(item) {
return dayjs(item.stop).toJSON()
}
function parseContent(content) {
const [, data] = content.match(/var pageData = ({.+?});/) || [null, null]
return data ? JSON.parse(data) : {}
}
function parseDirector(item) {
return item.credits && item.credits.director ? item.credits.director : null
}
function parseActor(item) {
return item.credits && item.credits.actor ? item.credits.actor : null
}
function parseRating(item) {
return item.rating
? {
system: 'CNC',
value: item.rating.toUpperCase()
}
: null
}
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'programetv.ro',
days: 2,
url: function ({ date, channel }) {
const daysOfWeek = {
0: 'duminica',
1: 'luni',
2: 'marti',
3: 'miercuri',
4: 'joi',
5: 'vineri',
6: 'sambata'
}
const day = date.day()
return `https://www.programetv.ro/program-tv/${channel.site_id}/${daysOfWeek[day]}/`
},
parser: function ({ content }) {
let programs = []
const data = parseContent(content)
if (!data || !data.shows) return programs
const items = data.shows
items.forEach(item => {
programs.push({
title: item.title,
sub_title: item.titleOriginal,
description: item.desc || item.obs,
category: item.categories,
season: item.season || null,
episode: item.episode || null,
start: parseStart(item),
stop: parseStop(item),
url: item.url || null,
date: item.date,
rating: parseRating(item),
directors: parseDirector(item),
actors: parseActor(item),
icon: item.icon
})
})
return programs
},
async channels() {
const axios = require('axios')
const data = await axios
.get('https://www.programetv.ro/api/station/index/')
.then(r => r.data)
.catch(console.log)
return data.map(item => {
return {
lang: 'ro',
site_id: item.slug,
name: item.displayName
}
})
}
}
function parseStart(item) {
return dayjs(item.start).toJSON()
}
function parseStop(item) {
return dayjs(item.stop).toJSON()
}
function parseContent(content) {
const [, data] = content.match(/var pageData = ({.+?});/) || [null, null]
return data ? JSON.parse(data) : {}
}
function parseDirector(item) {
return item.credits && item.credits.director ? item.credits.director : null
}
function parseActor(item) {
return item.credits && item.credits.actor ? item.credits.actor : null
}
function parseRating(item) {
return item.rating
? {
system: 'CNC',
value: item.rating.toUpperCase()
}
: null
}

View File

@@ -1,42 +1,42 @@
const { parser, url } = require('./programetv.ro.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('2021-10-24', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' }
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe('https://www.programetv.ro/program-tv/pro-tv/duminica/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-07T05:00:00.000Z',
stop: '2021-11-07T07:59:59.000Z',
title: 'Ştirile Pro Tv',
description:
'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.',
category: ['Ştiri'],
icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA=='
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./programetv.ro.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('2021-10-24', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' }
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe('https://www.programetv.ro/program-tv/pro-tv/duminica/')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-07T05:00:00.000Z',
stop: '2021-11-07T07:59:59.000Z',
title: 'Ştirile Pro Tv',
description:
'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.',
category: ['Ştiri'],
icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA=='
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,107 +1,107 @@
const { parser, url, request } = require('./programme-tv.vini.pf.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('2021-11-21', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'tf1',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
expect(url).toBe('https://programme-tv.vini.pf/programmesJSON')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request data', () => {
expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' })
})
it('can parse response', done => {
axios.post.mockImplementation((url, data) => {
if (data.dateDebut === '2021-11-20T16:00:00-10:00') {
return Promise.resolve({
data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json')))
})
} else {
return Promise.resolve({
data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json')))
})
}
})
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
parser({ content, channel, date })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-20T23:50:00.000Z',
stop: '2021-11-21T01:10:00.000Z',
title: 'Reportages découverte',
category: 'Magazine',
description:
"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.",
image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg'
},
{
start: '2021-11-21T01:10:00.000Z',
stop: '2021-11-21T02:30:00.000Z',
title: 'Les docs du week-end',
category: 'Magazine',
description:
'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?',
image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg'
},
{
start: '2021-11-21T02:30:00.000Z',
stop: '2021-11-21T03:45:00.000Z',
title: '50mn Inside',
category: 'Magazine',
description:
"50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,",
image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg'
}
])
done()
})
.catch(err => {
done(err)
})
})
it('can handle empty guide', done => {
parser({
date,
channel,
content:
''
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(err => {
done(err)
})
})
const { parser, url, request } = require('./programme-tv.vini.pf.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('2021-11-21', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'tf1',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
expect(url).toBe('https://programme-tv.vini.pf/programmesJSON')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request data', () => {
expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' })
})
it('can parse response', done => {
axios.post.mockImplementation((url, data) => {
if (data.dateDebut === '2021-11-20T16:00:00-10:00') {
return Promise.resolve({
data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json')))
})
} else {
return Promise.resolve({
data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json')))
})
}
})
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
parser({ content, channel, date })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-20T23:50:00.000Z',
stop: '2021-11-21T01:10:00.000Z',
title: 'Reportages découverte',
category: 'Magazine',
description:
"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.",
image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg'
},
{
start: '2021-11-21T01:10:00.000Z',
stop: '2021-11-21T02:30:00.000Z',
title: 'Les docs du week-end',
category: 'Magazine',
description:
'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?',
image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg'
},
{
start: '2021-11-21T02:30:00.000Z',
stop: '2021-11-21T03:45:00.000Z',
title: '50mn Inside',
category: 'Magazine',
description:
"50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,",
image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg'
}
])
done()
})
.catch(err => {
done(err)
})
})
it('can handle empty guide', done => {
parser({
date,
channel,
content:
''
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(err => {
done(err)
})
})

View File

@@ -1,76 +1,76 @@
const MockDate = require('mockdate')
const { parser, url } = require('./programtv.onet.pl.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('2021-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '13th-street-250',
xmltv_id: '13thStreet.de'
}
it('can generate valid url', () => {
MockDate.set(dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d'))
expect(url({ channel, date })).toBe(
'https://programtv.onet.pl/program-tv/13th-street-250?dzien=0'
)
MockDate.reset()
})
it('can generate valid url for next day', () => {
MockDate.set(dayjs.utc('2021-11-23', 'YYYY-MM-DD').startOf('d'))
expect(url({ channel, date })).toBe(
'https://programtv.onet.pl/program-tv/13th-street-250?dzien=1'
)
MockDate.reset()
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T02:20:00.000Z',
stop: '2021-11-24T22:30:00.000Z',
title: 'Law & Order, odc. 15: Letzte Worte',
category: 'Krimiserie',
description:
'Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....'
},
{
start: '2021-11-24T22:30:00.000Z',
stop: '2021-11-25T00:00:00.000Z',
title: 'Navy CIS, odc. 1: New Orleans',
category: 'Krimiserie',
description:
'Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...'
},
{
start: '2021-11-25T00:00:00.000Z',
stop: '2021-11-25T01:00:00.000Z',
title: 'Navy CIS: L.A, odc. 13: High Society',
category: 'Krimiserie',
description:
'Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
})
expect(result).toMatchObject([])
})
const MockDate = require('mockdate')
const { parser, url } = require('./programtv.onet.pl.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('2021-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '13th-street-250',
xmltv_id: '13thStreet.de'
}
it('can generate valid url', () => {
MockDate.set(dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d'))
expect(url({ channel, date })).toBe(
'https://programtv.onet.pl/program-tv/13th-street-250?dzien=0'
)
MockDate.reset()
})
it('can generate valid url for next day', () => {
MockDate.set(dayjs.utc('2021-11-23', 'YYYY-MM-DD').startOf('d'))
expect(url({ channel, date })).toBe(
'https://programtv.onet.pl/program-tv/13th-street-250?dzien=1'
)
MockDate.reset()
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T02:20:00.000Z',
stop: '2021-11-24T22:30:00.000Z',
title: 'Law & Order, odc. 15: Letzte Worte',
category: 'Krimiserie',
description:
'Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....'
},
{
start: '2021-11-24T22:30:00.000Z',
stop: '2021-11-25T00:00:00.000Z',
title: 'Navy CIS, odc. 1: New Orleans',
category: 'Krimiserie',
description:
'Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...'
},
{
start: '2021-11-25T00:00:00.000Z',
stop: '2021-11-25T01:00:00.000Z',
title: 'Navy CIS: L.A, odc. 13: High Society',
category: 'Krimiserie',
description:
'Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,50 +1,50 @@
const { parser, url } = require('./raiplay.it.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-05-03', 'YYYY-MM-DD')
const channel = {
site_id: 'rai-2',
xmltv_id: 'Rai2.it'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-05-03T17:40:00.000Z',
stop: '2022-05-03T18:30:00.000Z',
title: 'The Good Doctor S3E5 - La prima volta',
description:
"Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.",
season: '3',
episode: '5',
sub_title: 'La prima volta',
image: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg',
url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./raiplay.it.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-05-03', 'YYYY-MM-DD')
const channel = {
site_id: 'rai-2',
xmltv_id: 'Rai2.it'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-05-03T17:40:00.000Z',
stop: '2022-05-03T18:30:00.000Z',
title: 'The Good Doctor S3E5 - La prima volta',
description:
"Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.",
season: '3',
episode: '5',
sub_title: 'La prima volta',
image: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg',
url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,170 +1,170 @@
require('dayjs/locale/es')
const axios = require('axios')
const dayjs = require('dayjs')
const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const { startCase } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'reportv.com.ar',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
},
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data({ channel, date }) {
const formData = new URLSearchParams()
formData.append('idSenial', channel.site_id)
formData.append('Alineacion', '2694')
formData.append('DiaDesde', date.format('YYYY/MM/DD'))
formData.append('HoraDesde', '00:00:00')
return formData
}
},
url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php',
parser: async function ({ content, date }) {
let programs = []
const items = parseItems(content, date)
for (let item of items) {
const $item = cheerio.load(item)
const start = parseStart($item, date)
const duration = parseDuration($item)
const stop = start.add(duration, 's')
const details = await loadProgramDetails($item)
programs.push({
title: parseTitle($item),
category: parseCategory($item),
image: details.image,
description: details.description,
directors: details.directors,
actors: details.actors,
start,
stop
})
}
return programs
},
async channels() {
const content = await axios
.get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694')
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(content)
const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray()
return items.map(item => {
return {
lang: 'es',
site_id: $(item).attr('value'),
name: $(item).text()
}
})
}
}
async function loadProgramDetails($item) {
const onclick = $item('*').attr('onclick')
const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g
const match = [...onclick.matchAll(regexp)]
const [, id, idc, id_alineacion, idp, title] = match[0]
if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({})
const formData = new URLSearchParams()
formData.append('id', id)
formData.append('idc', idc)
formData.append('id_alineacion', id_alineacion)
formData.append('idp', idp)
formData.append('title', title)
const content = await axios
.post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData)
.then(r => r.data.toString())
.catch(console.error)
if (!content) return Promise.resolve({})
const $ = cheerio.load(content)
return Promise.resolve({
image: parseImage($),
actors: parseActors($),
directors: parseDirectors($),
description: parseDescription($)
})
}
function parseActors($) {
const section = $('#Ficha > div')
.html()
.split('<br>')
.find(str => str.includes('Actores:'))
if (!section) return null
const $section = cheerio.load(section)
return $section('span')
.map((i, el) => $(el).text().trim())
.get()
}
function parseDirectors($) {
const section = $('#Ficha > div')
.html()
.split('<br>')
.find(str => str.includes('Directores:'))
if (!section) return null
const $section = cheerio.load(section)
return $section('span')
.map((i, el) => $(el).text().trim())
.get()
}
function parseDescription($) {
return $('#Sinopsis > div').text().trim()
}
function parseImage($) {
const src = $('#ImgProg').attr('src')
const url = new URL(src, 'https://www.reportv.com.ar/buscador/')
return url.href
}
function parseTitle($item) {
const [, title] = $item('div:nth-child(1) > span').text().split(' - ')
return title
}
function parseCategory($item) {
return $item('div:nth-child(3) > span').text()
}
function parseStart($item, date) {
const [time] = $item('div:nth-child(1) > span').text().split(' - ')
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas')
}
function parseDuration($item) {
const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':')
return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss)
}
function parseItems(content, date) {
if (!content) return []
const $ = cheerio.load(content)
const d = startCase(date.locale('es').format('DD MMMM YYYY'))
return $(`.trProg[title*="${d}"]`).toArray()
}
require('dayjs/locale/es')
const axios = require('axios')
const dayjs = require('dayjs')
const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const { startCase } = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'reportv.com.ar',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
},
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data({ channel, date }) {
const formData = new URLSearchParams()
formData.append('idSenial', channel.site_id)
formData.append('Alineacion', '2694')
formData.append('DiaDesde', date.format('YYYY/MM/DD'))
formData.append('HoraDesde', '00:00:00')
return formData
}
},
url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php',
parser: async function ({ content, date }) {
let programs = []
const items = parseItems(content, date)
for (let item of items) {
const $item = cheerio.load(item)
const start = parseStart($item, date)
const duration = parseDuration($item)
const stop = start.add(duration, 's')
const details = await loadProgramDetails($item)
programs.push({
title: parseTitle($item),
category: parseCategory($item),
image: details.image,
description: details.description,
directors: details.directors,
actors: details.actors,
start,
stop
})
}
return programs
},
async channels() {
const content = await axios
.get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694')
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(content)
const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray()
return items.map(item => {
return {
lang: 'es',
site_id: $(item).attr('value'),
name: $(item).text()
}
})
}
}
async function loadProgramDetails($item) {
const onclick = $item('*').attr('onclick')
const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g
const match = [...onclick.matchAll(regexp)]
const [, id, idc, id_alineacion, idp, title] = match[0]
if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({})
const formData = new URLSearchParams()
formData.append('id', id)
formData.append('idc', idc)
formData.append('id_alineacion', id_alineacion)
formData.append('idp', idp)
formData.append('title', title)
const content = await axios
.post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData)
.then(r => r.data.toString())
.catch(console.error)
if (!content) return Promise.resolve({})
const $ = cheerio.load(content)
return Promise.resolve({
image: parseImage($),
actors: parseActors($),
directors: parseDirectors($),
description: parseDescription($)
})
}
function parseActors($) {
const section = $('#Ficha > div')
.html()
.split('<br>')
.find(str => str.includes('Actores:'))
if (!section) return null
const $section = cheerio.load(section)
return $section('span')
.map((i, el) => $(el).text().trim())
.get()
}
function parseDirectors($) {
const section = $('#Ficha > div')
.html()
.split('<br>')
.find(str => str.includes('Directores:'))
if (!section) return null
const $section = cheerio.load(section)
return $section('span')
.map((i, el) => $(el).text().trim())
.get()
}
function parseDescription($) {
return $('#Sinopsis > div').text().trim()
}
function parseImage($) {
const src = $('#ImgProg').attr('src')
const url = new URL(src, 'https://www.reportv.com.ar/buscador/')
return url.href
}
function parseTitle($item) {
const [, title] = $item('div:nth-child(1) > span').text().split(' - ')
return title
}
function parseCategory($item) {
return $item('div:nth-child(3) > span').text()
}
function parseStart($item, date) {
const [time] = $item('div:nth-child(1) > span').text().split(' - ')
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas')
}
function parseDuration($item) {
const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':')
return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss)
}
function parseItems(content, date) {
if (!content) return []
const $ = cheerio.load(content)
const d = startCase(date.locale('es').format('DD MMMM YYYY'))
return $(`.trProg[title*="${d}"]`).toArray()
}

View File

@@ -1,58 +1,58 @@
const { parser, url } = require('./rikstv.no.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-14', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '47',
xmltv_id: 'NRK1.no'
}
describe('rikstv.no Module Tests', () => {
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
`https://play.rikstv.no/api/content-search/1/channel/${channel.site_id}/epg/${date.format(
'YYYY-MM-DD'
)}`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = dayjs(p.start).toISOString()
p.stop = dayjs(p.stop).toISOString()
return p
})
expect(result).toMatchObject([
{
title: 'Vakre og ville Oman',
sub_title: 'Vakre og ville Oman',
description:
'Oman er eit arabisk skattkammer av unike habitat og variert dyreliv. Rev, kvalhai, reptil og skjelpadder er blant skapningane du finn her.',
season: 1,
episode: 1,
category: ['Dokumentar', 'Fakta', 'Natur'],
actors: ['Gergana Muskalla'],
directors: 'Stefania Muller',
icon: 'https://imageservice.rikstv.no/hash/EC206C374F42287C0BDF850A7D3CB4D3.jpg',
start: '2025-01-13T23:00:00.000Z',
stop: '2025-01-13T23:55:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})
})
const { parser, url } = require('./rikstv.no.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-14', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '47',
xmltv_id: 'NRK1.no'
}
describe('rikstv.no Module Tests', () => {
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
`https://play.rikstv.no/api/content-search/1/channel/${channel.site_id}/epg/${date.format(
'YYYY-MM-DD'
)}`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = dayjs(p.start).toISOString()
p.stop = dayjs(p.stop).toISOString()
return p
})
expect(result).toMatchObject([
{
title: 'Vakre og ville Oman',
sub_title: 'Vakre og ville Oman',
description:
'Oman er eit arabisk skattkammer av unike habitat og variert dyreliv. Rev, kvalhai, reptil og skjelpadder er blant skapningane du finn her.',
season: 1,
episode: 1,
category: ['Dokumentar', 'Fakta', 'Natur'],
actors: ['Gergana Muskalla'],
directors: 'Stefania Muller',
icon: 'https://imageservice.rikstv.no/hash/EC206C374F42287C0BDF850A7D3CB4D3.jpg',
start: '2025-01-13T23:00:00.000Z',
stop: '2025-01-13T23:55:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})
})

View File

@@ -1,48 +1,48 @@
const { parser, url } = require('./rtmklik.rtm.gov.my.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-09-04', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2',
xmltv_id: 'TV2.my'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-09-04T19:00:00.000Z',
stop: '2022-09-04T20:00:00.000Z',
title: 'Hope Of Life',
description:
'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./rtmklik.rtm.gov.my.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-09-04', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2',
xmltv_id: 'TV2.my'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-09-04T19:00:00.000Z',
stop: '2022-09-04T20:00:00.000Z',
title: 'Hope Of Life',
description:
'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,65 +1,65 @@
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 = {
lis: 'Europe/Lisbon',
per: 'Asia/Macau',
rja: 'America/Sao_Paulo'
}
module.exports = {
site: 'rtp.pt',
days: 2,
url({ channel, date }) {
let [region, channelCode] = channel.site_id.split('#')
return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format(
'D-M-YYYY'
)}/${region}`
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
const prev = programs[programs.length - 1]
let start = parseStart(item, channel)
if (!start) return
if (prev) {
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: item.name,
description: item.description,
image: parseImage(item),
start,
stop
})
})
return programs
}
}
function parseImage(item) {
const last = item.image.pop()
if (!last) return null
return last.src
}
function parseStart(item, channel) {
let [region] = channel.site_id.split('#')
return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region])
}
function parseItems(content) {
if (!content) return []
const data = JSON.parse(content)
return Object.values(data.result).flat()
}
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 = {
lis: 'Europe/Lisbon',
per: 'Asia/Macau',
rja: 'America/Sao_Paulo'
}
module.exports = {
site: 'rtp.pt',
days: 2,
url({ channel, date }) {
let [region, channelCode] = channel.site_id.split('#')
return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format(
'D-M-YYYY'
)}/${region}`
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
const prev = programs[programs.length - 1]
let start = parseStart(item, channel)
if (!start) return
if (prev) {
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: item.name,
description: item.description,
image: parseImage(item),
start,
stop
})
})
return programs
}
}
function parseImage(item) {
const last = item.image.pop()
if (!last) return null
return last.src
}
function parseStart(item, channel) {
let [region] = channel.site_id.split('#')
return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region])
}
function parseItems(content) {
if (!content) return []
const data = JSON.parse(content)
return Object.values(data.result).flat()
}

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./s.mxtv.jp.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-08-01', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2',
name: 'Tokyo MX2',
xmltv_id: 'TokyoMX2.jp'
}
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe('https://s.mxtv.jp/bangumi_file/json01/SV2EPG20240801.json')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2024-07-26T20:00:00.000Z', // UTC time
stop: '2024-07-26T21:00:00.000Z', // UTC
title: 'ヒーリングタイム&ヘッドラインニュース',
description: 'ねこの足跡',
image: null,
category: null
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '[]'
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./s.mxtv.jp.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-08-01', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2',
name: 'Tokyo MX2',
xmltv_id: 'TokyoMX2.jp'
}
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe('https://s.mxtv.jp/bangumi_file/json01/SV2EPG20240801.json')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2024-07-26T20:00:00.000Z', // UTC time
stop: '2024-07-26T21:00:00.000Z', // UTC
title: 'ヒーリングタイム&ヘッドラインニュース',
description: 'ねこの足跡',
image: null,
category: null
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '[]'
})
expect(result).toMatchObject([])
})

View File

@@ -1,41 +1,41 @@
const { url, parser } = require('./shahid.mbc.net.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('2023-11-11').startOf('d')
const channel = { site_id: '996520', xmltv_id: 'AlAanTV.ae', lang: 'en' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
`https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${
channel.site_id
}&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format(
'YYYY-MM-DD'
)}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date })
expect(result).toMatchObject([
{
start: '2023-11-10T21:00:00.000Z',
stop: '2023-11-10T21:30:00.000Z',
title: "Menassaatona Fi Osboo'",
description:
"The presenter reviews the most prominent episodes of news programs produced by the channel's team on a weekly basis, which include the most important global updates and developments at all levels."
}
])
})
it('can handle empty guide', () => {
const result = parser({ content: '' })
expect(result).toMatchObject([])
})
const { url, parser } = require('./shahid.mbc.net.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('2023-11-11').startOf('d')
const channel = { site_id: '996520', xmltv_id: 'AlAanTV.ae', lang: 'en' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
`https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${
channel.site_id
}&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format(
'YYYY-MM-DD'
)}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}`
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date })
expect(result).toMatchObject([
{
start: '2023-11-10T21:00:00.000Z',
stop: '2023-11-10T21:30:00.000Z',
title: "Menassaatona Fi Osboo'",
description:
"The presenter reviews the most prominent episodes of news programs produced by the channel's team on a weekly basis, which include the most important global updates and developments at all levels."
}
])
})
it('can handle empty guide', () => {
const result = parser({ content: '' })
expect(result).toMatchObject([])
})

View File

@@ -1,54 +1,54 @@
const { parser, url, request } = require('./siba.com.co.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('2021-11-11', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '395',
xmltv_id: 'CanalClaro.cl'
}
it('can generate valid url', () => {
expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla')
})
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 result = request.data({ channel, date })
expect(result.has('servicio')).toBe(true)
expect(result.has('ini')).toBe(true)
expect(result.has('end')).toBe(true)
expect(result.has('chn')).toBe(true)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-11T00:00:00.000Z',
stop: '2021-11-11T01:00:00.000Z',
title: 'Worst Cooks In America'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./siba.com.co.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('2021-11-11', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '395',
xmltv_id: 'CanalClaro.cl'
}
it('can generate valid url', () => {
expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla')
})
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 result = request.data({ channel, date })
expect(result.has('servicio')).toBe(true)
expect(result.has('ini')).toBe(true)
expect(result.has('end')).toBe(true)
expect(result.has('chn')).toBe(true)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-11T00:00:00.000Z',
stop: '2021-11-11T01:00:00.000Z',
title: 'Worst Cooks In America'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,85 +1,85 @@
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:sky.com')
const { sortBy } = require('../../scripts/functions')
dayjs.extend(utc)
doFetch.setDebugger(debug)
module.exports = {
site: 'sky.com',
days: 2,
url({ date, channel }) {
return `https://awk.epgsky.com/hawk/linear/schedule/${date.format('YYYYMMDD')}/${
channel.site_id
}`
},
parser({ content, channel, date }) {
const programs = []
if (content) {
const items = JSON.parse(content) || null
if (Array.isArray(items.schedule)) {
items.schedule
.filter(schedule => schedule.sid === channel.site_id)
.forEach(schedule => {
if (Array.isArray(schedule.events)) {
sortBy(schedule.events, p => p.st).forEach(event => {
const start = dayjs.utc(event.st * 1000)
if (start.isSame(date, 'd')) {
const image = `https://images.metadata.sky.com/pd-image/${event.programmeuuid}/16-9/640`
programs.push({
title: event.t,
description: event.sy,
season: event.seasonnumber,
episode: event.episodenumber,
start,
stop: start.add(event.d, 's'),
icon: image,
image
})
}
})
}
})
}
}
return programs
},
async channels() {
const channels = {}
const queues = [{ t: 'r', url: 'https://www.sky.com/tv-guide' }]
await doFetch(queues, (queue, res) => {
// process regions
if (queue.t === 'r') {
const $ = cheerio.load(res)
const initialData = JSON.parse(decodeURIComponent($('#initialData').text()))
initialData.state.epgData.regions.forEach(region => {
queues.push({
t: 'c',
url: `https://awk.epgsky.com/hawk/linear/services/${region.bouquet}/${region.subBouquet}`
})
})
}
// process channels
if (queue.t === 'c') {
if (Array.isArray(res.services)) {
for (const ch of res.services) {
if (channels[ch.sid] === undefined) {
channels[ch.sid] = {
lang: 'en',
site_id: ch.sid,
name: ch.t
}
}
}
}
}
})
return Object.values(channels)
}
}
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:sky.com')
const { sortBy } = require('../../scripts/functions')
dayjs.extend(utc)
doFetch.setDebugger(debug)
module.exports = {
site: 'sky.com',
days: 2,
url({ date, channel }) {
return `https://awk.epgsky.com/hawk/linear/schedule/${date.format('YYYYMMDD')}/${
channel.site_id
}`
},
parser({ content, channel, date }) {
const programs = []
if (content) {
const items = JSON.parse(content) || null
if (Array.isArray(items.schedule)) {
items.schedule
.filter(schedule => schedule.sid === channel.site_id)
.forEach(schedule => {
if (Array.isArray(schedule.events)) {
sortBy(schedule.events, p => p.st).forEach(event => {
const start = dayjs.utc(event.st * 1000)
if (start.isSame(date, 'd')) {
const image = `https://images.metadata.sky.com/pd-image/${event.programmeuuid}/16-9/640`
programs.push({
title: event.t,
description: event.sy,
season: event.seasonnumber,
episode: event.episodenumber,
start,
stop: start.add(event.d, 's'),
icon: image,
image
})
}
})
}
})
}
}
return programs
},
async channels() {
const channels = {}
const queues = [{ t: 'r', url: 'https://www.sky.com/tv-guide' }]
await doFetch(queues, (queue, res) => {
// process regions
if (queue.t === 'r') {
const $ = cheerio.load(res)
const initialData = JSON.parse(decodeURIComponent($('#initialData').text()))
initialData.state.epgData.regions.forEach(region => {
queues.push({
t: 'c',
url: `https://awk.epgsky.com/hawk/linear/services/${region.bouquet}/${region.subBouquet}`
})
})
}
// process channels
if (queue.t === 'c') {
if (Array.isArray(res.services)) {
for (const ch of res.services) {
if (channels[ch.sid] === undefined) {
channels[ch.sid] = {
lang: 'en',
site_id: ch.sid,
name: ch.t
}
}
}
}
}
})
return Object.values(channels)
}
}

View File

@@ -1,66 +1,66 @@
const { parser, url, request } = require('./sky.de.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('2022-02-28', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '522',
xmltv_id: 'WarnerTVComedyHD.de'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
cil: [channel.site_id],
d: date.valueOf()
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'King of Queens',
description: 'Der Experte',
category: 'Comedyserie',
start: '2022-02-26T23:05:00.000Z',
stop: '2022-02-26T23:30:00.000Z',
season: '4',
episode: '11',
image: 'http://sky.de/static/img/program_guide/1522936_s.jpg'
},
{
title: 'King of Queens',
description: 'Speedy Gonzales',
category: 'Comedyserie',
start: '2022-02-26T23:30:00.000Z',
stop: '2022-02-26T23:55:00.000Z',
season: '4',
episode: '12',
image: 'http://sky.de/static/img/program_guide/1522937_s.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./sky.de.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('2022-02-28', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '522',
xmltv_id: 'WarnerTVComedyHD.de'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
cil: [channel.site_id],
d: date.valueOf()
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'King of Queens',
description: 'Der Experte',
category: 'Comedyserie',
start: '2022-02-26T23:05:00.000Z',
stop: '2022-02-26T23:30:00.000Z',
season: '4',
episode: '11',
image: 'http://sky.de/static/img/program_guide/1522936_s.jpg'
},
{
title: 'King of Queens',
description: 'Speedy Gonzales',
category: 'Comedyserie',
start: '2022-02-26T23:30:00.000Z',
stop: '2022-02-26T23:55:00.000Z',
season: '4',
episode: '12',
image: 'http://sky.de/static/img/program_guide/1522937_s.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '[]'
})
expect(result).toMatchObject([])
})

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./stod2.is.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')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day')
const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' }
it('can generate valid url', () => {
const generatedUrl = url({ date, channel })
expect(generatedUrl).toBe('https://api.stod2.is/dagskra/api/stod2/2025-01-03')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toISOString()
p.stop = p.stop.toISOString()
return p
})
expect(result).toMatchObject([
{
title: 'Heimsókn',
sub_title: 'Telma Borgþórsdóttir',
description:
'Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.',
actors: '',
directors: '',
start: '2025-01-03T08:00:00.000Z',
stop: '2025-01-03T08:15:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({ content: '[]' })
expect(result).toMatchObject([])
})
const { parser, url } = require('./stod2.is.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')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day')
const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' }
it('can generate valid url', () => {
const generatedUrl = url({ date, channel })
expect(generatedUrl).toBe('https://api.stod2.is/dagskra/api/stod2/2025-01-03')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toISOString()
p.stop = p.stop.toISOString()
return p
})
expect(result).toMatchObject([
{
title: 'Heimsókn',
sub_title: 'Telma Borgþórsdóttir',
description:
'Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.',
actors: '',
directors: '',
start: '2025-01-03T08:00:00.000Z',
stop: '2025-01-03T08:15:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({ content: '[]' })
expect(result).toMatchObject([])
})

View File

@@ -1,97 +1,97 @@
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
const { sortBy, uniqBy } = require('../../scripts/functions')
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
module.exports = {
site: 'streamingtvguides.com',
days: 2,
url({ channel }) {
return `https://streamingtvguides.com/Channel/${channel.site_id}`
},
parser({ content, date }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
const $item = cheerio.load(item)
const start = parseStart($item)
if (!date.isSame(start, 'd')) return
programs.push({
title: parseTitle($item),
description: parseDescription($item),
start,
stop: parseStop($item)
})
})
programs = sortBy(uniqBy(programs, p => p.start), p => p.start.valueOf())
return programs
},
async channels() {
const axios = require('axios')
const data = await axios
.get('https://streamingtvguides.com/Preferences')
.then(r => r.data)
.catch(console.log)
let channels = []
const $ = cheerio.load(data)
$('#channel-group-all > div > div').each((i, el) => {
const site_id = $(el).find('input').attr('value').replace('&', '&amp;')
const label = $(el).text().trim()
const svgTitle = $(el).find('svg').attr('alt')
const name = (label || svgTitle || '').replace(site_id, '').trim()
if (!name || !site_id) return
channels.push({
lang: 'en',
site_id,
name
})
})
return channels
}
}
function parseTitle($item) {
return $item('.card-body > .prog-contains > .card-title')
.clone()
.children()
.remove()
.end()
.text()
.trim()
}
function parseDescription($item) {
return $item('.card-body > .card-text').clone().children().remove().end().text().trim()
}
function parseStart($item) {
const date = $item('.card-body').clone().children().remove().end().text().trim()
const [time] = date.split(' - ')
return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc()
}
function parseStop($item) {
const date = $item('.card-body').clone().children().remove().end().text().trim()
const [, time] = date.split(' - ')
return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('.container').toArray()
}
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone')
const { sortBy, uniqBy } = require('../../scripts/functions')
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
module.exports = {
site: 'streamingtvguides.com',
days: 2,
url({ channel }) {
return `https://streamingtvguides.com/Channel/${channel.site_id}`
},
parser({ content, date }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
const $item = cheerio.load(item)
const start = parseStart($item)
if (!date.isSame(start, 'd')) return
programs.push({
title: parseTitle($item),
description: parseDescription($item),
start,
stop: parseStop($item)
})
})
programs = sortBy(uniqBy(programs, p => p.start), p => p.start.valueOf())
return programs
},
async channels() {
const axios = require('axios')
const data = await axios
.get('https://streamingtvguides.com/Preferences')
.then(r => r.data)
.catch(console.log)
let channels = []
const $ = cheerio.load(data)
$('#channel-group-all > div > div').each((i, el) => {
const site_id = $(el).find('input').attr('value').replace('&', '&amp;')
const label = $(el).text().trim()
const svgTitle = $(el).find('svg').attr('alt')
const name = (label || svgTitle || '').replace(site_id, '').trim()
if (!name || !site_id) return
channels.push({
lang: 'en',
site_id,
name
})
})
return channels
}
}
function parseTitle($item) {
return $item('.card-body > .prog-contains > .card-title')
.clone()
.children()
.remove()
.end()
.text()
.trim()
}
function parseDescription($item) {
return $item('.card-body > .card-text').clone().children().remove().end().text().trim()
}
function parseStart($item) {
const date = $item('.card-body').clone().children().remove().end().text().trim()
const [time] = date.split(' - ')
return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc()
}
function parseStop($item) {
const date = $item('.card-body').clone().children().remove().end().text().trim()
const [, time] = date.split(' - ')
return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('.container').toArray()
}

View File

@@ -1,43 +1,43 @@
const { url, parser } = require('./taiwanplus.com.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('2023-08-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'TaiwanPlusTV.tw',
lang: 'en',
logo: 'https://i.imgur.com/SfcZyqm.png'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, date })
expect(results).toMatchObject([
{
title: 'Master Class',
start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'),
stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'),
description:
'From blockchain to Buddha statues, Taiwans culture is a kaleidoscope of old and new just waiting to be discovered.',
image: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp',
category: 'TaiwanPlus ✕ Discovery',
rating: '0+'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
const { url, parser } = require('./taiwanplus.com.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('2023-08-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'TaiwanPlusTV.tw',
lang: 'en',
logo: 'https://i.imgur.com/SfcZyqm.png'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, date })
expect(results).toMatchObject([
{
title: 'Master Class',
start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'),
stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'),
description:
'From blockchain to Buddha statues, Taiwans culture is a kaleidoscope of old and new just waiting to be discovered.',
image: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp',
category: 'TaiwanPlus ✕ Discovery',
rating: '0+'
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./tapdmv.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-10-04', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756',
xmltv_id: 'TAPActionFlix.ph'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-10-04T01:00:00.000Z',
stop: '2022-10-04T02:25:00.000Z',
title: 'The Devil Inside',
description:
'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.',
category: 'Horror',
image: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
date
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tapdmv.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-10-04', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756',
xmltv_id: 'TAPActionFlix.ph'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-10-04T01:00:00.000Z',
stop: '2022-10-04T02:25:00.000Z',
title: 'The Devil Inside',
description:
'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.',
category: 'Horror',
image: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
date
})
expect(result).toMatchObject([])
})

View File

@@ -1,82 +1,82 @@
const axios = require('axios')
module.exports = {
site: 'tataplay.com',
days: 1,
url({ date }) {
return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format(
'DD-MM-YYYY'
)}`
},
request: {
method: 'POST',
headers: {
Accept: '*/*',
Origin: 'https://watch.tataplay.com',
Referer: 'https://watch.tataplay.com/',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'content-type': 'application/json',
locale: 'ENG',
platform: 'web'
},
data({ channel }) {
return { id: channel.site_id }
}
},
parser(context) {
let data = []
try {
const json = JSON.parse(context.content)
const programs = json?.data?.epg || []
data = programs.map(program => ({
title: program.title,
start: program.startTime,
stop: program.endTime,
description: program.desc,
category: program.category,
icon: program.boxCoverImage
}))
} catch {
data = []
}
return data
},
async channels() {
const headers = {
Accept: '*/*',
Origin: 'https://watch.tataplay.com',
Referer: 'https://watch.tataplay.com/',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'content-type': 'application/json',
locale: 'ENG',
platform: 'web'
}
const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule'
const initialUrl = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=0`
const initialResponse = await axios.get(initialUrl, { headers })
const total = initialResponse.data?.data?.total || 0
const channels = []
for (let offset = 0; offset < total; offset += 20) {
const url = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=${offset}`
const response = await axios.get(url, { headers })
const page = response.data?.data?.channelList || []
channels.push(...page)
}
return channels.map(channel => ({
site_id: channel.id,
name: channel.title,
lang: 'en',
icon: channel.transparentImageUrl || channel.thumbnailImage
}))
}
}
const axios = require('axios')
module.exports = {
site: 'tataplay.com',
days: 1,
url({ date }) {
return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format(
'DD-MM-YYYY'
)}`
},
request: {
method: 'POST',
headers: {
Accept: '*/*',
Origin: 'https://watch.tataplay.com',
Referer: 'https://watch.tataplay.com/',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'content-type': 'application/json',
locale: 'ENG',
platform: 'web'
},
data({ channel }) {
return { id: channel.site_id }
}
},
parser(context) {
let data = []
try {
const json = JSON.parse(context.content)
const programs = json?.data?.epg || []
data = programs.map(program => ({
title: program.title,
start: program.startTime,
stop: program.endTime,
description: program.desc,
category: program.category,
icon: program.boxCoverImage
}))
} catch {
data = []
}
return data
},
async channels() {
const headers = {
Accept: '*/*',
Origin: 'https://watch.tataplay.com',
Referer: 'https://watch.tataplay.com/',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'content-type': 'application/json',
locale: 'ENG',
platform: 'web'
}
const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule'
const initialUrl = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=0`
const initialResponse = await axios.get(initialUrl, { headers })
const total = initialResponse.data?.data?.total || 0
const channels = []
for (let offset = 0; offset < total; offset += 20) {
const url = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=${offset}`
const response = await axios.get(url, { headers })
const page = response.data?.data?.channelList || []
channels.push(...page)
}
return channels.map(channel => ({
site_id: channel.id,
name: channel.title,
lang: 'en',
icon: channel.transparentImageUrl || channel.thumbnailImage
}))
}
}

View File

@@ -1,89 +1,89 @@
const { parser, url, channels } = require('./tataplay.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-06-09', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '1001' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=09-06-2025'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, date })
expect(results.length).toBe(2)
expect(results[0]).toMatchObject({
title: 'Yeh Rishta Kya Kehlata Hai',
start: '2025-06-09T18:00:00.000Z',
stop: '2025-06-09T18:30:00.000Z',
description: 'The story of the Rajshri family and their journey through life.',
category: 'Drama',
icon: 'https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg'
})
expect(results[1]).toMatchObject({
title: 'Anupamaa',
start: '2025-06-09T18:30:00.000Z',
stop: '2025-06-09T19:00:00.000Z',
description: 'The story of Anupamaa, a housewife who rediscovers herself.',
category: 'Drama',
icon: 'https://img.tataplay.com/thumbnails/1001/anupamaa.jpg'
})
})
it('can handle empty guide', () => {
const content = JSON.stringify({ data: { epg: [] } })
const results = parser({ content, date })
expect(results).toMatchObject([])
})
it('can parse channel list', async () => {
const mockResponse = {
data: {
data: {
total: 2,
channelList: [
{
id: '1001',
title: 'Star Plus',
transparentImageUrl: 'https://img.tataplay.com/channels/1001/logo.png'
},
{
id: '1002',
title: 'Sony TV',
transparentImageUrl: 'https://img.tataplay.com/channels/1002/logo.png'
}
]
}
}
}
// Mock axios.get to return our test data
const axios = require('axios')
axios.get = jest.fn().mockResolvedValue(mockResponse)
const results = await channels()
expect(results.length).toBe(2)
expect(results[0]).toMatchObject({
site_id: '1001',
name: 'Star Plus',
lang: 'en',
icon: 'https://img.tataplay.com/channels/1001/logo.png'
})
expect(results[1]).toMatchObject({
site_id: '1002',
name: 'Sony TV',
lang: 'en',
icon: 'https://img.tataplay.com/channels/1002/logo.png'
})
})
const { parser, url, channels } = require('./tataplay.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-06-09', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '1001' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=09-06-2025'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, date })
expect(results.length).toBe(2)
expect(results[0]).toMatchObject({
title: 'Yeh Rishta Kya Kehlata Hai',
start: '2025-06-09T18:00:00.000Z',
stop: '2025-06-09T18:30:00.000Z',
description: 'The story of the Rajshri family and their journey through life.',
category: 'Drama',
icon: 'https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg'
})
expect(results[1]).toMatchObject({
title: 'Anupamaa',
start: '2025-06-09T18:30:00.000Z',
stop: '2025-06-09T19:00:00.000Z',
description: 'The story of Anupamaa, a housewife who rediscovers herself.',
category: 'Drama',
icon: 'https://img.tataplay.com/thumbnails/1001/anupamaa.jpg'
})
})
it('can handle empty guide', () => {
const content = JSON.stringify({ data: { epg: [] } })
const results = parser({ content, date })
expect(results).toMatchObject([])
})
it('can parse channel list', async () => {
const mockResponse = {
data: {
data: {
total: 2,
channelList: [
{
id: '1001',
title: 'Star Plus',
transparentImageUrl: 'https://img.tataplay.com/channels/1001/logo.png'
},
{
id: '1002',
title: 'Sony TV',
transparentImageUrl: 'https://img.tataplay.com/channels/1002/logo.png'
}
]
}
}
}
// Mock axios.get to return our test data
const axios = require('axios')
axios.get = jest.fn().mockResolvedValue(mockResponse)
const results = await channels()
expect(results.length).toBe(2)
expect(results[0]).toMatchObject({
site_id: '1001',
name: 'Star Plus',
lang: 'en',
icon: 'https://img.tataplay.com/channels/1001/logo.png'
})
expect(results[1]).toMatchObject({
site_id: '1002',
name: 'Sony TV',
lang: 'en',
icon: 'https://img.tataplay.com/channels/1002/logo.png'
})
})

View File

@@ -1,60 +1,60 @@
const { parser, url } = require('./teliatv.ee.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('2021-11-20', 'YYYY-MM-DD').startOf('d')
const channel = {
lang: 'et',
site_id: 'et#1',
xmltv_id: 'ETV.ee'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt'
)
})
it('can generate valid url with different language', () => {
const ruChannel = {
lang: 'ru',
site_id: 'ru#1',
xmltv_id: 'ETV.ee'
}
expect(url({ date, channel: ruChannel })).toBe(
'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-19T22:05:00.000Z',
stop: '2021-11-19T22:55:00.000Z',
title: 'Inimjaht',
image:
'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./teliatv.ee.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('2021-11-20', 'YYYY-MM-DD').startOf('d')
const channel = {
lang: 'et',
site_id: 'et#1',
xmltv_id: 'ETV.ee'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt'
)
})
it('can generate valid url with different language', () => {
const ruChannel = {
lang: 'ru',
site_id: 'ru#1',
xmltv_id: 'ETV.ee'
}
expect(url({ date, channel: ruChannel })).toBe(
'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-19T22:05:00.000Z',
stop: '2021-11-19T22:55:00.000Z',
title: 'Inimjaht',
image:
'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,72 +1,72 @@
const { parser, url } = require('./tv.blue.ch.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const fs = require('fs')
const path = require('path')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1221',
xmltv_id: 'BlueZoomD.ch'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-16T23:30:00.000Z',
stop: '2022-01-17T00:00:00.000Z',
title: 'Weekend on the Rocks',
description:
' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.',
image:
'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp'
}
])
})
it('can parse response without image', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_without_image.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-17T04:59:00.000Z',
stop: '2022-01-17T05:00:00.000Z',
title: 'Lorem ipsum'
}
])
})
it('can handle wrong site id', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/content_invalid_siteid.json'))
})
expect(result).toMatchObject([])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tv.blue.ch.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const fs = require('fs')
const path = require('path')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1221',
xmltv_id: 'BlueZoomD.ch'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-16T23:30:00.000Z',
stop: '2022-01-17T00:00:00.000Z',
title: 'Weekend on the Rocks',
description:
' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.',
image:
'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp'
}
])
})
it('can parse response without image', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_without_image.json'))
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-17T04:59:00.000Z',
stop: '2022-01-17T05:00:00.000Z',
title: 'Lorem ipsum'
}
])
})
it('can handle wrong site id', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/content_invalid_siteid.json'))
})
expect(result).toMatchObject([])
})
it('can handle empty guide', () => {
const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,103 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="tv.dir.bg" lang="bg" xmltv_id="24Kitchen.us@Bulgaria" site_id="96">24 Kitchen</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="78TV.bg" site_id="98">7/8 TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AlJazeeraBalkans.ba" site_id="9">Al Jazeera</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AnimalPlanetEurope.uk" site_id="23">Animal Planet</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="92">AXN</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AXNBlack.us" site_id="43">AXN Black</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AXNWhite.us" site_id="36">AXN White</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BabyTV.uk" site_id="79">Baby TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BBCNews.uk" site_id="49">BBC News (former BBC World News)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BloombergTVBulgaria.bg" site_id="65">Bloomberg TV Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT1.bg" site_id="57">BNT1 (БНТ1)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT2.bg" site_id="99">BNT2 (БНТ2)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT3.bg" site_id="82">BNT3 (БНТ3)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT4.bg" site_id="64">BNT4 (БНТ4)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTV.bg" site_id="61">bTV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVAction.bg" site_id="95">bTV Action</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVCinema.bg" site_id="58">bTV Cinema</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVComedy.bg" site_id="81">bTV Comedy</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVLady.bg" site_id="74">bTV Story (f.k.a bTV Lady)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BulgariaOnAir.bg" site_id="100">Bulgaria ON AIR (България Он Еър)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="84">Cartoon Network Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CartoonitoWesternEurope.uk" site_id="80">Cartoonito (f.k.a Boomerang TV)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Cinemania.rs" site_id="54">Cinemania</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CinemaxCentralEurope.hu@HD" site_id="21">Cinemax</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Cinemax2CentralEurope.hu@HD" site_id="14">Cinemax 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="29">CineStar TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="33">CineStar TV Action&amp;Thriller</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CNN.us" site_id="47">CNN</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CodeFashionTV.bg" site_id="10">Code Fashion TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CodeHealthTV.bg" site_id="11">Code Health TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CrimePlusInvestigation.uk" site_id="48">Crime &amp; Investigation</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Diema.bg" site_id="63">Diema</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaFamily.bg" site_id="90">Diema Family</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaSport.bg" site_id="16">Diema Sport</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaSport2.bg" site_id="31">Diema Sport 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaSport3.bg" site_id="51">Diema Sport 3</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiscoveryChannel.bg" site_id="4">Discovery Channel</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DisneyChannel.bg" site_id="70">Disney Channel</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="20">Dizi (Timeless Drama Channel)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="87">Duck TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DW.de" site_id="7">DW TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="EpicDrama.uk@Sweden" site_id="45">Epic Drama</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurocom.bg" site_id="66">Eurocom</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="EuronewsEnglish.fr" site_id="46">Euronews</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TVEvropa.bg" site_id="91">Euronews Bulgaria (f.k.a Evropa TV)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurosport1.fr" site_id="67">Eurosport</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurosport2.fr" site_id="50">Eurosport 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurosport4K.fr" site_id="37">Eurosport 4K</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FightKlub.pl@Bulgaria" site_id="24">Fightklub HD (Bulgaria)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FilmBox.nl@Bulgaria" site_id="18">FilmBox Basic</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FilmBoxExtra.nl@Bulgaria" site_id="34">FilmBox Extra</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FilmBoxStars.nl@Bulgaria" site_id="35">FilmBox Stars (FilmBox Plus)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FoodNetworkEMEA.us" site_id="40">Food Network HD</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="France24.fr" site_id="1">France 24</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HBOCentralEurope.hu" site_id="42">HBO</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HBO2CentralEurope.hu" site_id="17">HBO2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HBO3CentralEurope.hu" site_id="15">HBO3</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HGTVLatinAmerica.us@Panregional" site_id="22">HGTV (Discovery Home &amp; Garden)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="38">History Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="InvestigationDiscoveryEurope.us" site_id="55">ID (Investigation Discovery)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Kanal3.bg" site_id="13">Kanal 3 (Канал 3)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NovaNews.bg" site_id="76">Kanal 4 (Канал 4)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="KinoNova.bg" site_id="86">Kino Nova</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="LoveNature.ca" site_id="93">Love Nature</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="75">Magic TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport1.bg" site_id="41">MAX Sport 1</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport2.bg" site_id="56">MAX Sport 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport3.bg" site_id="28">MAX Sport 3</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport4.bg" site_id="12">MAX Sport 4</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MovieStar.bg" site_id="2">MovieSTAR</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MTVGlobal.uk" site_id="25">MTV Europe</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NationalGeographic.bg" site_id="83">National Geographic</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NationalGeographicWild.bg" site_id="8">National Geographic Wild</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NickJr.bg" site_id="97">Nick Jr</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nickelodeon.bg" site_id="78">Nickelodeon</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nicktoons.bg" site_id="94">Nicktoons</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nostalgia.bg" site_id="26">Nostalgia TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NovaNews.bg" site_id="85">Nova News HD</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NovaSport.bg" site_id="62">NOVA Sport</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nova.bg" site_id="60">NOVA TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="RING.bg" site_id="59">Ring.bg (bTV Sport)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="RTL.de" site_id="6">RTL</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SKAT.bg" site_id="5">SKAT TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SkyShowtime1.fi@Bulgaria" site_id="53">Skyshowtime 1</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SkyShowtime2.fi@Bulgaria" site_id="52">Skyshowtime 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="StarChannel.bg" site_id="69">STAR Channel (f.k.a. FOX)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="StarCrime.bg" site_id="3">STAR Crime (f.k.a FOX Crime)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="StarLife.bg" site_id="30">STAR Life (f.k.a. FOX Life)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SuperToons.bg" site_id="27">Super Toons</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="History2.pl@Bulgaria" site_id="39">The History Channel 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="88">The Voice TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TLCBalkan.us" site_id="19">TLC</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TravelChannelEMEA.uk" site_id="72">Travel Channel</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TV1.bg" site_id="71">TV1 Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatExplore.se" site_id="73">Viasat Explore</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatHistory.se" site_id="68">Viasat History</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="vijuTV1000.ru" site_id="32">Viasat Kino (TV1000)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatNature.se" site_id="77">Viasat Nature</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatTrueCrime.pl@Bulgaria" site_id="89">Viasat True Crime</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="VivacomArena.bg" site_id="44">Vivacom Arena (Виваком Арена)</channel>
</channels>
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="tv.dir.bg" lang="bg" xmltv_id="24Kitchen.us@Bulgaria" site_id="96">24 Kitchen</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="78TV.bg" site_id="98">7/8 TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AlJazeeraBalkans.ba" site_id="9">Al Jazeera</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AnimalPlanetEurope.uk" site_id="23">Animal Planet</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="92">AXN</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AXNBlack.us" site_id="43">AXN Black</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="AXNWhite.us" site_id="36">AXN White</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BabyTV.uk" site_id="79">Baby TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BBCNews.uk" site_id="49">BBC News (former BBC World News)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BloombergTVBulgaria.bg" site_id="65">Bloomberg TV Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT1.bg" site_id="57">BNT1 (БНТ1)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT2.bg" site_id="99">BNT2 (БНТ2)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT3.bg" site_id="82">BNT3 (БНТ3)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BNT4.bg" site_id="64">BNT4 (БНТ4)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTV.bg" site_id="61">bTV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVAction.bg" site_id="95">bTV Action</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVCinema.bg" site_id="58">bTV Cinema</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVComedy.bg" site_id="81">bTV Comedy</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="bTVLady.bg" site_id="74">bTV Story (f.k.a bTV Lady)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="BulgariaOnAir.bg" site_id="100">Bulgaria ON AIR (България Он Еър)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="84">Cartoon Network Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CartoonitoWesternEurope.uk" site_id="80">Cartoonito (f.k.a Boomerang TV)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Cinemania.rs" site_id="54">Cinemania</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CinemaxCentralEurope.hu@HD" site_id="21">Cinemax</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Cinemax2CentralEurope.hu@HD" site_id="14">Cinemax 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="29">CineStar TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="33">CineStar TV Action&amp;Thriller</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CNN.us" site_id="47">CNN</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CodeFashionTV.bg" site_id="10">Code Fashion TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CodeHealthTV.bg" site_id="11">Code Health TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="CrimePlusInvestigation.uk" site_id="48">Crime &amp; Investigation</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Diema.bg" site_id="63">Diema</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaFamily.bg" site_id="90">Diema Family</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaSport.bg" site_id="16">Diema Sport</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaSport2.bg" site_id="31">Diema Sport 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiemaSport3.bg" site_id="51">Diema Sport 3</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DiscoveryChannel.bg" site_id="4">Discovery Channel</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DisneyChannel.bg" site_id="70">Disney Channel</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="20">Dizi (Timeless Drama Channel)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="87">Duck TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="DW.de" site_id="7">DW TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="EpicDrama.uk@Sweden" site_id="45">Epic Drama</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurocom.bg" site_id="66">Eurocom</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="EuronewsEnglish.fr" site_id="46">Euronews</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TVEvropa.bg" site_id="91">Euronews Bulgaria (f.k.a Evropa TV)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurosport1.fr" site_id="67">Eurosport</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurosport2.fr" site_id="50">Eurosport 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Eurosport4K.fr" site_id="37">Eurosport 4K</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FightKlub.pl@Bulgaria" site_id="24">Fightklub HD (Bulgaria)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FilmBox.nl@Bulgaria" site_id="18">FilmBox Basic</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FilmBoxExtra.nl@Bulgaria" site_id="34">FilmBox Extra</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FilmBoxStars.nl@Bulgaria" site_id="35">FilmBox Stars (FilmBox Plus)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="FoodNetworkEMEA.us" site_id="40">Food Network HD</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="France24.fr" site_id="1">France 24</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HBOCentralEurope.hu" site_id="42">HBO</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HBO2CentralEurope.hu" site_id="17">HBO2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HBO3CentralEurope.hu" site_id="15">HBO3</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="HGTVLatinAmerica.us@Panregional" site_id="22">HGTV (Discovery Home &amp; Garden)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="38">History Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="InvestigationDiscoveryEurope.us" site_id="55">ID (Investigation Discovery)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Kanal3.bg" site_id="13">Kanal 3 (Канал 3)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NovaNews.bg" site_id="76">Kanal 4 (Канал 4)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="KinoNova.bg" site_id="86">Kino Nova</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="LoveNature.ca" site_id="93">Love Nature</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="75">Magic TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport1.bg" site_id="41">MAX Sport 1</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport2.bg" site_id="56">MAX Sport 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport3.bg" site_id="28">MAX Sport 3</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MaxSport4.bg" site_id="12">MAX Sport 4</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MovieStar.bg" site_id="2">MovieSTAR</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="MTVGlobal.uk" site_id="25">MTV Europe</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NationalGeographic.bg" site_id="83">National Geographic</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NationalGeographicWild.bg" site_id="8">National Geographic Wild</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NickJr.bg" site_id="97">Nick Jr</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nickelodeon.bg" site_id="78">Nickelodeon</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nicktoons.bg" site_id="94">Nicktoons</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nostalgia.bg" site_id="26">Nostalgia TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NovaNews.bg" site_id="85">Nova News HD</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="NovaSport.bg" site_id="62">NOVA Sport</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="Nova.bg" site_id="60">NOVA TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="RING.bg" site_id="59">Ring.bg (bTV Sport)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="RTL.de" site_id="6">RTL</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SKAT.bg" site_id="5">SKAT TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SkyShowtime1.fi@Bulgaria" site_id="53">Skyshowtime 1</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SkyShowtime2.fi@Bulgaria" site_id="52">Skyshowtime 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="StarChannel.bg" site_id="69">STAR Channel (f.k.a. FOX)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="StarCrime.bg" site_id="3">STAR Crime (f.k.a FOX Crime)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="StarLife.bg" site_id="30">STAR Life (f.k.a. FOX Life)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="SuperToons.bg" site_id="27">Super Toons</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="History2.pl@Bulgaria" site_id="39">The History Channel 2</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="" site_id="88">The Voice TV</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TLCBalkan.us" site_id="19">TLC</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TravelChannelEMEA.uk" site_id="72">Travel Channel</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="TV1.bg" site_id="71">TV1 Bulgaria</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatExplore.se" site_id="73">Viasat Explore</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatHistory.se" site_id="68">Viasat History</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="vijuTV1000.ru" site_id="32">Viasat Kino (TV1000)</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatNature.se" site_id="77">Viasat Nature</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="ViasatTrueCrime.pl@Bulgaria" site_id="89">Viasat True Crime</channel>
<channel site="tv.dir.bg" lang="bg" xmltv_id="VivacomArena.bg" site_id="44">Vivacom Arena (Виваком Арена)</channel>
</channels>

View File

@@ -1,216 +1,216 @@
const axios = require('axios')
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)
let sessionCache = null
async function getSession(forceRefresh = false) {
if (sessionCache && !forceRefresh) {
return sessionCache
}
try {
const initResponse = await axios.get('https://tv.dir.bg/init')
if (!initResponse.data) {
throw new Error('No response data from init endpoint')
}
// Extract cookies from response headers
const setCookieHeader = initResponse.headers['set-cookie']
let xsrfToken = null
let dirSessionCookie = null
if (setCookieHeader) {
setCookieHeader.forEach(cookie => {
// Extract XSRF token from cookie
const xsrfMatch = cookie.match(/XSRF-TOKEN=([^;]+)/)
if (xsrfMatch) {
xsrfToken = decodeURIComponent(xsrfMatch[1])
}
// Extract dir_session cookie
const sessionMatch = cookie.match(/dir_session=([^;]+)/)
if (sessionMatch) {
dirSessionCookie = sessionMatch[1]
}
})
}
const csrfToken = initResponse.data.csrfToken
if (!csrfToken) {
throw new Error('No CSRF/XSRF token found in response')
}
// Build cookie string
let cookieString = ''
if (xsrfToken) {
cookieString += `XSRF-TOKEN=${encodeURIComponent(xsrfToken)}`
}
if (dirSessionCookie) {
if (cookieString) cookieString += '; '
cookieString += `dir_session=${dirSessionCookie}`
}
sessionCache = {
csrfToken,
cookieString,
timestamp: Date.now()
}
return sessionCache
} catch (error) {
console.error('Error getting session:', error.message)
throw error
}
}
module.exports = {
site: 'tv.dir.bg',
days: 2,
url: 'https://tv.dir.bg/load/programs',
request: {
maxContentLength: 125000000, // 10 MB
method: 'POST',
async headers() {
try {
const session = await getSession()
return {
'Cookie': session.cookieString,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
}
} catch (error) {
console.error('Error getting headers:', error.message)
throw error
}
},
async data({ channel, date }) {
try {
const session = await getSession()
const params = new URLSearchParams()
params.append('_token', session.csrfToken)
params.append('channel', channel.site_id)
params.append('day', date.format('YYYY-MM-DD'))
return params
} catch (error) {
console.error('Error preparing request data:', error.message)
throw error
}
},
},
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 (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: parseTitle($item),
start,
stop
})
})
return programs
},
async channels() {
try {
const response = await axios.get('https://tv.dir.bg/channels')
const $ = cheerio.load(response.data)
const channels = []
$('.channel_cont').each((_index, element) => {
const $element = $(element)
const $link = $element.find('a.channel_link')
const href = $link.attr('href')
const $img = $element.find('img')
const name = $img.attr('alt')
const logo = $img.attr('src')
const site_id = href ? href.match(/\/programa\/(\d+)/)?.[1] : ''
if (site_id && name) {
channels.push({
lang: 'bg',
site_id: site_id,
name: name.trim(),
logo: logo ? (logo.startsWith('http') ? logo : `https://tv.dir.bg${logo}`) : null
})
}
})
return channels
} catch (error) {
console.error('Error fetching channels:', error.message)
return []
}
},
clearSession() {
sessionCache = null
}
}
function parseStart($item, date) {
const time = $item('.broadcast-time').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${time}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Sofia')
}
function parseTitle($item) {
return $item('.broadcast-title').text()
.replace(/\s+/g, ' ')
.trim()
}
function parseItems(content) {
try {
const json = JSON.parse(content)
if (!json || json.status !== true) {
return []
}
const $ = cheerio.load(json.html)
const items = $('.broadcast-item').toArray()
return items
} catch (error) {
console.error('❌ Error parsing items:', error.message)
console.error('Error stack:', error.stack)
return []
}
const axios = require('axios')
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)
let sessionCache = null
async function getSession(forceRefresh = false) {
if (sessionCache && !forceRefresh) {
return sessionCache
}
try {
const initResponse = await axios.get('https://tv.dir.bg/init')
if (!initResponse.data) {
throw new Error('No response data from init endpoint')
}
// Extract cookies from response headers
const setCookieHeader = initResponse.headers['set-cookie']
let xsrfToken = null
let dirSessionCookie = null
if (setCookieHeader) {
setCookieHeader.forEach(cookie => {
// Extract XSRF token from cookie
const xsrfMatch = cookie.match(/XSRF-TOKEN=([^;]+)/)
if (xsrfMatch) {
xsrfToken = decodeURIComponent(xsrfMatch[1])
}
// Extract dir_session cookie
const sessionMatch = cookie.match(/dir_session=([^;]+)/)
if (sessionMatch) {
dirSessionCookie = sessionMatch[1]
}
})
}
const csrfToken = initResponse.data.csrfToken
if (!csrfToken) {
throw new Error('No CSRF/XSRF token found in response')
}
// Build cookie string
let cookieString = ''
if (xsrfToken) {
cookieString += `XSRF-TOKEN=${encodeURIComponent(xsrfToken)}`
}
if (dirSessionCookie) {
if (cookieString) cookieString += '; '
cookieString += `dir_session=${dirSessionCookie}`
}
sessionCache = {
csrfToken,
cookieString,
timestamp: Date.now()
}
return sessionCache
} catch (error) {
console.error('Error getting session:', error.message)
throw error
}
}
module.exports = {
site: 'tv.dir.bg',
days: 2,
url: 'https://tv.dir.bg/load/programs',
request: {
maxContentLength: 125000000, // 10 MB
method: 'POST',
async headers() {
try {
const session = await getSession()
return {
'Cookie': session.cookieString,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
}
} catch (error) {
console.error('Error getting headers:', error.message)
throw error
}
},
async data({ channel, date }) {
try {
const session = await getSession()
const params = new URLSearchParams()
params.append('_token', session.csrfToken)
params.append('channel', channel.site_id)
params.append('day', date.format('YYYY-MM-DD'))
return params
} catch (error) {
console.error('Error preparing request data:', error.message)
throw error
}
},
},
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 (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: parseTitle($item),
start,
stop
})
})
return programs
},
async channels() {
try {
const response = await axios.get('https://tv.dir.bg/channels')
const $ = cheerio.load(response.data)
const channels = []
$('.channel_cont').each((_index, element) => {
const $element = $(element)
const $link = $element.find('a.channel_link')
const href = $link.attr('href')
const $img = $element.find('img')
const name = $img.attr('alt')
const logo = $img.attr('src')
const site_id = href ? href.match(/\/programa\/(\d+)/)?.[1] : ''
if (site_id && name) {
channels.push({
lang: 'bg',
site_id: site_id,
name: name.trim(),
logo: logo ? (logo.startsWith('http') ? logo : `https://tv.dir.bg${logo}`) : null
})
}
})
return channels
} catch (error) {
console.error('Error fetching channels:', error.message)
return []
}
},
clearSession() {
sessionCache = null
}
}
function parseStart($item, date) {
const time = $item('.broadcast-time').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${time}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Sofia')
}
function parseTitle($item) {
return $item('.broadcast-title').text()
.replace(/\s+/g, ' ')
.trim()
}
function parseItems(content) {
try {
const json = JSON.parse(content)
if (!json || json.status !== true) {
return []
}
const $ = cheerio.load(json.html)
const items = $('.broadcast-item').toArray()
return items
} catch (error) {
console.error('❌ Error parsing items:', error.message)
console.error('Error stack:', error.stack)
return []
}
}

View File

@@ -1,50 +1,50 @@
const { parser, url } = require('./tv.dir.bg.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-06-30', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '61',
xmltv_id: 'BTV.bg'
}
it('can generate valid url', () => {
expect(url).toBe('https://tv.dir.bg/load/programs')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(63)
expect(results[0]).toMatchObject({
start: '2025-06-30T03:00:00.000Z',
stop: '2025-06-30T03:30:00.000Z',
title: 'Светът на здравето'
})
expect(results[62]).toMatchObject({
start: '2025-07-01T02:00:00.000Z',
stop: '2025-07-01T02:30:00.000Z',
title: 'Убийства в Рая , сезон 1 , епизод 7'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tv.dir.bg.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-06-30', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '61',
xmltv_id: 'BTV.bg'
}
it('can generate valid url', () => {
expect(url).toBe('https://tv.dir.bg/load/programs')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(63)
expect(results[0]).toMatchObject({
start: '2025-06-30T03:00:00.000Z',
stop: '2025-06-30T03:30:00.000Z',
title: 'Светът на здравето'
})
expect(results[62]).toMatchObject({
start: '2025-07-01T02:00:00.000Z',
stop: '2025-07-01T02:30:00.000Z',
title: 'Убийства в Рая , сезон 1 , епизод 7'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})

View File

@@ -1,54 +1,54 @@
const { parser, url } = require('./tv.lv.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-11-30', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ltv1',
xmltv_id: 'LTV1.lv'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.tv.lv/programme/listing/none/30-11-2023?filter=channel&subslug=ltv1'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(40)
expect(results[0]).toMatchObject({
start: '2023-11-29T22:05:00.000Z',
stop: '2023-11-29T22:35:00.000Z',
title: 'Ielas garumā. Pārdaugavas koka arhitektūra',
description: '',
category: ''
})
expect(results[39]).toMatchObject({
start: '2023-11-30T21:30:00.000Z',
stop: '2023-11-30T22:30:00.000Z',
title: 'Latvijas Sirdsdziesma',
description: '',
category: ''
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(results).toMatchObject([])
})
const { parser, url } = require('./tv.lv.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-11-30', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ltv1',
xmltv_id: 'LTV1.lv'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.tv.lv/programme/listing/none/30-11-2023?filter=channel&subslug=ltv1'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(40)
expect(results[0]).toMatchObject({
start: '2023-11-29T22:05:00.000Z',
stop: '2023-11-29T22:35:00.000Z',
title: 'Ielas garumā. Pārdaugavas koka arhitektūra',
description: '',
category: ''
})
expect(results[39]).toMatchObject({
start: '2023-11-30T21:30:00.000Z',
stop: '2023-11-30T22:30:00.000Z',
title: 'Latvijas Sirdsdziesma',
description: '',
category: ''
})
})
it('can handle empty guide', () => {
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(results).toMatchObject([])
})

View File

@@ -1,147 +1,147 @@
const axios = require('axios')
const crypto = require('crypto')
const dayjs = require('dayjs')
const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost'
const headers = {
'Device-Id': crypto.randomUUID(),
app_key: 'CTnKA63ruKM0JM1doxAXwwyQLLmQiEiy',
app_version: '02.0.1260',
'X-User-Agent': 'web|web|Firefox-120|02.0.1260|1',
'x-request-tracking-id': crypto.randomUUID()
}
module.exports = {
site: 'tv.magenta.at',
days: 2,
request: {
headers,
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url: function ({ channel, date }) {
return `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${
channel.site_id
}&date=${date.format('YYYY-MM-DD')}&hour_offset=${date.format('H')}&hour_range=3&natco_code=at`
},
async parser({ content, channel, date }) {
let programs = []
if (!content) return programs
let items = parseItems(JSON.parse(content), channel)
if (!items.length) return programs
const promises = [3, 6, 9, 12, 15, 18, 21].map(i =>
axios.get(
`${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${channel.site_id}&date=${date.format(
'YYYY-MM-DD'
)}&hour_offset=${i}&hour_range=3&natco_code=at`,
{ headers }
)
)
await Promise.allSettled(promises)
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') {
const parsed = parseItems(r.value.data, channel)
items = items.concat(parsed)
}
})
})
.catch(console.error)
for (let item of items) {
const detail = await loadProgramDetails(item)
programs.push({
title: item.description,
description: parseDescription(detail),
date: parseDate(item),
category: parseCategory(item),
image: detail.poster_image_url,
actors: parseRoles(detail, 'Schauspieler'),
directors: parseRoles(detail, 'Regisseur'),
producers: parseRoles(detail, 'Produzent'),
season: parseSeason(item),
episode: parseEpisode(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
},
async channels() {
const data = await axios
.get(`${API_ENDPOINT}/epg/channel?natco_code=at`, { headers })
.then(r => r.data)
.catch(console.log)
return data.channels.map(item => {
return {
lang: 'de',
site_id: item.station_id,
name: item.title
}
})
}
}
async function loadProgramDetails(item) {
if (!item.program_id) return {}
const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=at`
const data = await axios
.get(url, { headers })
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDate(item) {
return item && item.release_year ? item.release_year.toString() : null
}
function parseStart(item) {
return dayjs(item.start_time)
}
function parseStop(item) {
return dayjs(item.end_time)
}
function parseItems(data, channel) {
if (!data || !data.channels) return []
const channelData = data.channels[channel.site_id]
if (!channelData) return []
return channelData
}
function parseCategory(item) {
if (!item.genres) return null
return item.genres.map(genre => genre.id)
}
function parseSeason(item) {
if (item.season_display_number === 'Folgen') return null
return item.season_number
}
function parseEpisode(item) {
if (item.episode_number) return parseInt(item.episode_number)
if (item.season_display_number === 'Folgen') return item.season_number
return null
}
function parseDescription(item) {
if (!item.details) return null
return item.details.description
}
function parseRoles(item, role_name) {
if (!item.roles) return null
return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name)
}
const axios = require('axios')
const crypto = require('crypto')
const dayjs = require('dayjs')
const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost'
const headers = {
'Device-Id': crypto.randomUUID(),
app_key: 'CTnKA63ruKM0JM1doxAXwwyQLLmQiEiy',
app_version: '02.0.1260',
'X-User-Agent': 'web|web|Firefox-120|02.0.1260|1',
'x-request-tracking-id': crypto.randomUUID()
}
module.exports = {
site: 'tv.magenta.at',
days: 2,
request: {
headers,
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url: function ({ channel, date }) {
return `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${
channel.site_id
}&date=${date.format('YYYY-MM-DD')}&hour_offset=${date.format('H')}&hour_range=3&natco_code=at`
},
async parser({ content, channel, date }) {
let programs = []
if (!content) return programs
let items = parseItems(JSON.parse(content), channel)
if (!items.length) return programs
const promises = [3, 6, 9, 12, 15, 18, 21].map(i =>
axios.get(
`${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${channel.site_id}&date=${date.format(
'YYYY-MM-DD'
)}&hour_offset=${i}&hour_range=3&natco_code=at`,
{ headers }
)
)
await Promise.allSettled(promises)
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') {
const parsed = parseItems(r.value.data, channel)
items = items.concat(parsed)
}
})
})
.catch(console.error)
for (let item of items) {
const detail = await loadProgramDetails(item)
programs.push({
title: item.description,
description: parseDescription(detail),
date: parseDate(item),
category: parseCategory(item),
image: detail.poster_image_url,
actors: parseRoles(detail, 'Schauspieler'),
directors: parseRoles(detail, 'Regisseur'),
producers: parseRoles(detail, 'Produzent'),
season: parseSeason(item),
episode: parseEpisode(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
},
async channels() {
const data = await axios
.get(`${API_ENDPOINT}/epg/channel?natco_code=at`, { headers })
.then(r => r.data)
.catch(console.log)
return data.channels.map(item => {
return {
lang: 'de',
site_id: item.station_id,
name: item.title
}
})
}
}
async function loadProgramDetails(item) {
if (!item.program_id) return {}
const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=at`
const data = await axios
.get(url, { headers })
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDate(item) {
return item && item.release_year ? item.release_year.toString() : null
}
function parseStart(item) {
return dayjs(item.start_time)
}
function parseStop(item) {
return dayjs(item.end_time)
}
function parseItems(data, channel) {
if (!data || !data.channels) return []
const channelData = data.channels[channel.site_id]
if (!channelData) return []
return channelData
}
function parseCategory(item) {
if (!item.genres) return null
return item.genres.map(genre => genre.id)
}
function parseSeason(item) {
if (item.season_display_number === 'Folgen') return null
return item.season_number
}
function parseEpisode(item) {
if (item.episode_number) return parseInt(item.episode_number)
if (item.season_display_number === 'Folgen') return item.season_number
return null
}
function parseDescription(item) {
if (!item.details) return null
return item.details.description
}
function parseRoles(item, role_name) {
if (!item.roles) return null
return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name)
}

View File

@@ -1,122 +1,122 @@
const { DateTime } = require('luxon')
const axios = require('axios')
const { uniqBy } = require('../../scripts/functions')
module.exports = {
site: 'tv.mail.ru',
days: 2,
delay: 1000,
url({ channel, date }) {
return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${
channel.site_id
}&date=${date.format('YYYY-MM-DD')}`
},
parser({ content, date }) {
const programs = []
const items = parseItems(content)
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.name,
category: parseCategory(item),
start,
stop
})
})
return programs
},
async channels() {
const regions = [5506, 1096, 1125, 285]
let channels = []
for (let region of regions) {
const totalPages = await getTotalPageCount(region)
const pages = Array.from(Array(totalPages).keys())
for (let page of pages) {
const data = await axios
.get('https://tv.mail.ru/ajax/channel/list/', {
params: { page },
headers: {
cookie: `s=fver=0|geo=${region};`
}
})
.then(r => r.data)
.catch(console.log)
data.channels.forEach(item => {
channels.push({
lang: 'ru',
name: item.name,
site_id: item.id
})
})
}
}
return uniqBy(channels, 'site_id')
}
}
async function getTotalPageCount(region) {
const data = await axios
.get('https://tv.mail.ru/ajax/channel/list/', {
params: { page: 0 },
headers: {
cookie: `s=fver=0|geo=${region};`
}
})
.then(r => r.data)
.catch(console.log)
return data.total
}
function parseStart(item, date) {
const dateString = `${date.format('YYYY-MM-DD')} ${item.start}`
return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC()
}
function parseCategory(item) {
const categories = {
1: 'Фильм',
2: 'Сериал',
6: 'Документальное',
7: 'Телемагазин',
8: 'Позновательное',
10: 'Другое',
14: 'ТВ-шоу',
16: 'Досуг,Хобби',
17: 'Ток-шоу',
18: 'Юмористическое',
23: 'Музыка',
24: 'Развлекательное',
25: 'Игровое',
26: 'Новости'
}
return categories[item.category_id]
? {
lang: 'ru',
value: categories[item.category_id]
}
: null
}
function parseItems(content) {
const json = JSON.parse(content)
if (!Array.isArray(json.schedule) || !json.schedule[0]) return []
const event = json.schedule[0].event || []
return [...event.past, ...event.current]
}
const { DateTime } = require('luxon')
const axios = require('axios')
const { uniqBy } = require('../../scripts/functions')
module.exports = {
site: 'tv.mail.ru',
days: 2,
delay: 1000,
url({ channel, date }) {
return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${
channel.site_id
}&date=${date.format('YYYY-MM-DD')}`
},
parser({ content, date }) {
const programs = []
const items = parseItems(content)
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.name,
category: parseCategory(item),
start,
stop
})
})
return programs
},
async channels() {
const regions = [5506, 1096, 1125, 285]
let channels = []
for (let region of regions) {
const totalPages = await getTotalPageCount(region)
const pages = Array.from(Array(totalPages).keys())
for (let page of pages) {
const data = await axios
.get('https://tv.mail.ru/ajax/channel/list/', {
params: { page },
headers: {
cookie: `s=fver=0|geo=${region};`
}
})
.then(r => r.data)
.catch(console.log)
data.channels.forEach(item => {
channels.push({
lang: 'ru',
name: item.name,
site_id: item.id
})
})
}
}
return uniqBy(channels, 'site_id')
}
}
async function getTotalPageCount(region) {
const data = await axios
.get('https://tv.mail.ru/ajax/channel/list/', {
params: { page: 0 },
headers: {
cookie: `s=fver=0|geo=${region};`
}
})
.then(r => r.data)
.catch(console.log)
return data.total
}
function parseStart(item, date) {
const dateString = `${date.format('YYYY-MM-DD')} ${item.start}`
return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC()
}
function parseCategory(item) {
const categories = {
1: 'Фильм',
2: 'Сериал',
6: 'Документальное',
7: 'Телемагазин',
8: 'Позновательное',
10: 'Другое',
14: 'ТВ-шоу',
16: 'Досуг,Хобби',
17: 'Ток-шоу',
18: 'Юмористическое',
23: 'Музыка',
24: 'Развлекательное',
25: 'Игровое',
26: 'Новости'
}
return categories[item.category_id]
? {
lang: 'ru',
value: categories[item.category_id]
}
: null
}
function parseItems(content) {
const json = JSON.parse(content)
if (!Array.isArray(json.schedule) || !json.schedule[0]) return []
const event = json.schedule[0].event || []
return [...event.past, ...event.current]
}

View File

@@ -1,77 +1,77 @@
const { parser, url } = require('./tv.mail.ru.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const fs = require('fs')
const path = require('path')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2785',
xmltv_id: '21TV.am'
}
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json'), 'utf8')
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24'
)
})
it('can parse response', () => {
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T20:35:00.000Z',
stop: '2021-11-24T22:40:00.000Z',
title: 'Նոնստոպ․ Տեսահոլովակներ',
category: {
lang: 'ru',
value: 'Музыка'
}
},
{
start: '2021-11-24T22:40:00.000Z',
stop: '2021-11-24T23:40:00.000Z',
title: 'Վերջին թագավորությունը',
category: {
lang: 'ru',
value: 'Сериал'
}
},
{
start: '2021-11-24T23:40:00.000Z',
stop: '2021-11-25T00:25:00.000Z',
title: 'Պրոֆեսիոնալները',
category: {
lang: 'ru',
value: 'Позновательное'
}
},
{
start: '2021-11-25T00:25:00.000Z',
stop: '2021-11-25T01:25:00.000Z',
title: 'Նոնստոպ․ Տեսահոլովակներ',
category: {
lang: 'ru',
value: 'Музыка'
}
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tv.mail.ru.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const fs = require('fs')
const path = require('path')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '2785',
xmltv_id: '21TV.am'
}
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json'), 'utf8')
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24'
)
})
it('can parse response', () => {
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-24T20:35:00.000Z',
stop: '2021-11-24T22:40:00.000Z',
title: 'Նոնստոպ․ Տեսահոլովակներ',
category: {
lang: 'ru',
value: 'Музыка'
}
},
{
start: '2021-11-24T22:40:00.000Z',
stop: '2021-11-24T23:40:00.000Z',
title: 'Վերջին թագավորությունը',
category: {
lang: 'ru',
value: 'Сериал'
}
},
{
start: '2021-11-24T23:40:00.000Z',
stop: '2021-11-25T00:25:00.000Z',
title: 'Պրոֆեսիոնալները',
category: {
lang: 'ru',
value: 'Позновательное'
}
},
{
start: '2021-11-25T00:25:00.000Z',
stop: '2021-11-25T01:25:00.000Z',
title: 'Նոնստոպ․ Տեսահոլովակներ',
category: {
lang: 'ru',
value: 'Музыка'
}
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.join(__dirname, '__data__', 'no_content.json'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,92 +1,92 @@
const { parser, url, request } = require('./tv.yandex.ru.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-11-26').startOf('d')
const channel = {
site_id: '16',
xmltv_id: 'ChannelOne.ru'
}
axios.get.mockImplementation(url => {
if (url === 'https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') {
return Promise.resolve({
headers: {},
data: fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
})
}
if (url === 'https://tv.yandex.ru/api/120809?date=2023-11-26&grid=all&period=all-day') {
return Promise.resolve({
headers: {},
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json')))
})
}
if (
url ===
'https://tv.yandex.ru/api/120809/main/chunk?page=0&date=2023-11-26&period=all-day&offset=0&limit=11'
) {
return Promise.resolve({
headers: {},
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule0.json')))
})
}
if (url === 'https://tv.yandex.ru/api/120809/event?eventId=217749657&programCoId=') {
return Promise.resolve({
headers: {},
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json')))
})
}
})
it('can generate valid url', () => {
expect(url({ date })).toBe('https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
Cookie:
'i=eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=; ' +
'spravka=dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1; ' +
'yandexuid=1197179041732383499; ' +
'yashr=4682342911732383504; ' +
'yuidss=1197179041732383499; ' +
'user_display=824'
})
})
it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const result = (await parser({ content, date, channel })).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-11-26T01:35:00.000Z',
stop: '2023-11-26T02:10:00.000Z',
title: 'ПОДКАСТ.ЛАБ. Мелодии моей жизни',
category: 'досуг',
description:
'Впереди вся ночь и есть о чем поговорить. Фильмы, музыка, любовь, звезды, еда, мода, анекдоты, спорт, деньги, настоящее, будущее - все это в творческом эксперименте.\nЛариса Гузеева читает любовные письма. Леонид Якубович рассказывает, кого не берут в пилоты. Арина Холина - какой секс способен довести до мужа или до развода. Валерий Сюткин на ходу сочиняет песню для Карины Кросс и Вали Карнавал. Дмитрий Дибров дарит новую жизнь любимой "Антропологии". Денис Казанский - все о футболе, хоккее и не только.\n"ПОДКАСТЫ. ЛАБ" - серия подкастов разной тематики, которые невозможно проспать. Интеллектуальные дискуссии после полуночи с самыми компетентными экспертами и актуальными спикерами.'
}
])
})
it('can handle empty guide', async () => {
const result = await parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__', 'no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./tv.yandex.ru.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-11-26').startOf('d')
const channel = {
site_id: '16',
xmltv_id: 'ChannelOne.ru'
}
axios.get.mockImplementation(url => {
if (url === 'https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') {
return Promise.resolve({
headers: {},
data: fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
})
}
if (url === 'https://tv.yandex.ru/api/120809?date=2023-11-26&grid=all&period=all-day') {
return Promise.resolve({
headers: {},
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json')))
})
}
if (
url ===
'https://tv.yandex.ru/api/120809/main/chunk?page=0&date=2023-11-26&period=all-day&offset=0&limit=11'
) {
return Promise.resolve({
headers: {},
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule0.json')))
})
}
if (url === 'https://tv.yandex.ru/api/120809/event?eventId=217749657&programCoId=') {
return Promise.resolve({
headers: {},
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json')))
})
}
})
it('can generate valid url', () => {
expect(url({ date })).toBe('https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
Cookie:
'i=eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=; ' +
'spravka=dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1; ' +
'yandexuid=1197179041732383499; ' +
'yashr=4682342911732383504; ' +
'yuidss=1197179041732383499; ' +
'user_display=824'
})
})
it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const result = (await parser({ content, date, channel })).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2023-11-26T01:35:00.000Z',
stop: '2023-11-26T02:10:00.000Z',
title: 'ПОДКАСТ.ЛАБ. Мелодии моей жизни',
category: 'досуг',
description:
'Впереди вся ночь и есть о чем поговорить. Фильмы, музыка, любовь, звезды, еда, мода, анекдоты, спорт, деньги, настоящее, будущее - все это в творческом эксперименте.\nЛариса Гузеева читает любовные письма. Леонид Якубович рассказывает, кого не берут в пилоты. Арина Холина - какой секс способен довести до мужа или до развода. Валерий Сюткин на ходу сочиняет песню для Карины Кросс и Вали Карнавал. Дмитрий Дибров дарит новую жизнь любимой "Антропологии". Денис Казанский - все о футболе, хоккее и не только.\n"ПОДКАСТЫ. ЛАБ" - серия подкастов разной тематики, которые невозможно проспать. Интеллектуальные дискуссии после полуночи с самыми компетентными экспертами и актуальными спикерами.'
}
])
})
it('can handle empty guide', async () => {
const result = await parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__', 'no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,338 +1,338 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="14">RTS Maribor</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="17">TV Veseljak Golica</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="26">Discovery Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="110">Hayat Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000028">AMC</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000043">History Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000048">History Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000074">Disney Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000080">Folk Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000155">Disney Junior</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000181">Alfa TV MAK</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000223">MTV 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000224">Naša TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000267">Alfa TV BiH</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000283">INFO TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000291">Animal Planet</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000302">Nickelodeon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000470">TV Nakupi</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000477">HBO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000478">HBO 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000479">HBO 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000480">Cinemax</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000481">Cinemax 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000503">RT Doc</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000505">Discovery Science</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000506">DTX</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000507">ID</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000517">Freedom</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000547">Filmbox Premium</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000615">E!</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000765">Pink Serije</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000766">Pink Koncert</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000767">PinknRoll</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000792">Sonce TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000793">Prva World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000794">Prva Max</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000795">Happy Reality</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000796">Happy Reality 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000797">Prva Files</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000798">Prva Kick</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000799">Prva Life</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000800">Prva Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000801">Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000802">One Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000803">Folx Slovenija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000807">BBC News</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000808">Dom TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000812">Espreso TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000813">Duck TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000814">Non Stop</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000815">Hit TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000816">Bloomberg Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000817">Arena Sport 1 Premium</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000818">Megafon TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000820">CineStar TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000821">LH TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000825">English Club TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000826">Harmonika TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000827">GLAM</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000828">XXXTazy</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000829">Angels</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000830">BooB</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000831">Capable Hole</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000832">Devils Home</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000833">Foxy Dolls</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000834">MIxxx</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000835">Prva TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000868">BIR TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000870">KIC TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000871">Kitchen TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000874">Mediaset Italia</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000875">TgCom24</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000001">R Kanal+</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000039">Cartoon Network</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000041">RTV Shqiptar</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000045">Europe by Satellite</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000050">RTV Atlas</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="3sat.de" site_id="1000640">3SAT</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="24Kitchen.us@Slovenia" site_id="1000307">24Kitchen Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="360TuneBox.nl" site_id="1000558">360 Tunebox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AgroTV.rs" site_id="1000686">Agro TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AlJazeeraBalkans.ba" site_id="1000180">Al Jazeera Balkans</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Alsat.mk" site_id="1000219">Alsat Macedoniae</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AMCEurope.uk@Portugal@Slovenia" site_id="1000523">AMC</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AnixeHDSerie.de" site_id="1000057">Anixe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaEsport.rs" site_id="1000683">TV Arena Esport</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaFight.rs" site_id="1000777">Arena Fight</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport1.rs" site_id="1000688">Arena Sport 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport2.rs" site_id="1000689">Arena Sport 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport3.rs" site_id="1000787">Arena Sport 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport4.rs" site_id="1000788">Arena Sport 4</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="arte.fr" site_id="1000026">Arte</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ATMTV.si" site_id="1000001">ATM Kranjska Gora</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="B92.rs" site_id="1000207">B92</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BabyTV.uk" site_id="82">Baby TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BalkanErotic.si" site_id="1000645">Balkan Erotic</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BalkanikaTV.bg" site_id="2000000027">Balkanika Music TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BalkanTripTV.rs" site_id="1000685">TV Balkan Trip</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BBCEarth.uk@Romania" site_id="1000482">BBC Earth</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BBCFirst.uk@Poland" site_id="1000652">BBC First</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BHT1.ba" site_id="1000182">BHT 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BKTV.si" site_id="1000265">BK TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BN2.ba" site_id="129">BN TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BNMusic.ba" site_id="1000067">BN Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CartoonitoCEE.uk" site_id="2000000032">Cartoonito</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BoomTV.si" site_id="124">Aktual TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BRIO.si" site_id="1000368">BRIO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Carousel.ru" site_id="1000273">Karousel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CBSRealityEMEA.uk" site_id="44">CBS Reality</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CGTN.cn" site_id="125">CGTN</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ChannelOne.ru" site_id="1000272">Channel One Russia</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTV1.rs" site_id="1000085">CineStar TV 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVAction.rs" site_id="1000427">Cinestar Action &amp; Thriller</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVComedy.hr" site_id="1000617">Cinestar Comedy</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVFantasy.hr" site_id="1000618">Cinestar Fantasy</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVPremiere1.hr" site_id="1000431">Cinestar Premiere</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVPremiere2.hr" site_id="1000430">Cinestar Premiere 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ClubMTVEurope.uk" site_id="126">Club MTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CMCTV.hr" site_id="1000156">CMC - Croatian Music Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CNNInternational.us@MENA" site_id="25">CNN International</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CrimePlusInvestigation.uk" site_id="1000153">Crime &amp; Investigation Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DasErste.de" site_id="1000059">Das Erste (ARD)</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DaVinci.de" site_id="1000055">Da Vinci</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DivaAdria.us" site_id="2000000040">Diva</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DMSat.rs" site_id="2000000026">DM SAT Televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DocuBox.nl" site_id="1000551">Docubox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Domkino.ru" site_id="1000274">Dom Kino</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DorcelXXX.nl" site_id="1000486">Dorcel TV XXX</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Duna.hu" site_id="1000648">Duna</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DunaWorld.hu" site_id="1000786">Duna World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DuskTV.nl" site_id="1000373">Dusk</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ELTA2.ba" site_id="1000179">Elta 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ELTA1HD.ba" site_id="1000240">Elta TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EpicDrama.uk@Sweden" site_id="1000501">Epic Drama</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ePosavjeTV.si" site_id="1000651">ePosavje TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EroX.nl" site_id="1000559">Erox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EroXXX.nl" site_id="1000553">Eroxxx</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ETV.si" site_id="1000641">ETV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EuronewsEnglish.fr" site_id="1000764">Euronews</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport1.fr@Germany" site_id="1000044">Eurosport (NEM)</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport1.fr" site_id="1000025">Eurosport</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport2.fr" site_id="1000504">Eurosport 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport4K.fr" site_id="1000728">Eurosport</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EWTN.us@Europe" site_id="36">EWTN Europe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ExodusTV.si" site_id="1000455">Exodus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Extreme.si" site_id="1000646">Extreme</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ExtremeSportsChannel.nl" site_id="37">Extreme Sports</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FashionBox.nl" site_id="1000556">Fashionbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FashionTVEurope.fr" site_id="1000056">Fashion TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FastFunBox.nl" site_id="1000552">Fastnfunbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Federalnatelevizija.ba" site_id="1000183">FTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FenFolkTV.bg" site_id="1000545">FenFolk TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FenTV.bg" site_id="1000544">FEN TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FightBox.nl" site_id="1000550">Fightbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FilmBoxArthouse.nl" site_id="1000557">Filmbox Art House</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FilmBoxExtra.nl@Bulgaria" site_id="1000548">Filmbox Extra</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FilmBoxStars.nl@Bulgaria" site_id="1000549">Filmbox Stars</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Fox.rs" site_id="1000308">STAR</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FoxCrime.rs" site_id="1000310">STAR Crime</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FoxLife.rs" site_id="1000309">STAR Life</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FoxMovies.si" site_id="1000311">STAR Movies</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="France2.fr" site_id="1000639">FR2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="France24.fr@English" site_id="1000159">France 24 English</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="France24.fr@French" site_id="1000158">France 24 French</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FunBoxUHD.nl" site_id="1000521">Funbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Gametoon.nl" site_id="1000554">Gametoon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="GeaTV.si" site_id="1000034">Gea TV Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="GOLDTV.si" site_id="1000407">GOLD TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="GolicaTV.si" site_id="2000000056">TV Zlati zvoki</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Happy.rs" site_id="1000209">Happy TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Hayat.ba" site_id="1000236">Hayat</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HayatFolk.ba" site_id="1000284">Hayat Folk</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HemaTV.ba" site_id="1000233">Hema</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HGTVLatinAmerica.us@Panregional" site_id="1000730">HGTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="History2.pl" site_id="1000619">H2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HotPleasure.si" site_id="1000644">Hot Pleasure</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HotXXL.si" site_id="1000643">Hot XXL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HRT1.hr" site_id="105">HRT 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HRT2.hr" site_id="106">HRT 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HustlerHD.nl" site_id="1000306">Hustler TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HustlerTVEurope.nl" site_id="1000018">Hustler TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="JimJamEurope.uk" site_id="1000509">Jim Jam</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000285">Jugoton TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="kabeleins.de" site_id="61">Kabel 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Kanal5.mk" site_id="1000268">Kanal 5</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="KanalA.si" site_id="1000370">Kanal A</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Kanali7.al" site_id="1000174">Tring 7</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="KINO.si" site_id="1000366">KINO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Klasik.hr" site_id="1000154">Klasik</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="KoroskaTV.si" site_id="1000474">Koroška TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="M1.hu" site_id="1000647">M1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="M2.hu" site_id="1000050">M2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="M5.hu" site_id="1000650">M5</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Mezzo.fr" site_id="91">Mezzo</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MezzoLive.fr" site_id="1000518">Mezzo Live</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MilfTV.si" site_id="1000491">Milf TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MinimaxCEE.cz" site_id="1000262">Minimax</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MrezaTV.hr" site_id="1000193">Mreža TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MRT1.mk" site_id="1000222">MTV 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MRT2.mk" site_id="1000199">MTV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTV00s.uk" site_id="50">MTV 00s</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTV80s.uk" site_id="51">MTV 80s</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTV90s.uk" site_id="136">MTV 90s</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTVGlobal.uk" site_id="1000496">MTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTVHitsEurope.uk" site_id="127">MTV Hits</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTVLive.uk" site_id="1000497">MTV Live</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MuzykaPervogo.ru" site_id="1000275">Muzika Pervogo</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NarodnaTV.rs" site_id="1000269">Narodna TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NationalGeographic.si" site_id="1000049">National Geographic</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NationalGeographicWild.si" site_id="1000167">National Geographic Wild</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NetTV.si" site_id="12">Net TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NetXXL.si" site_id="1000228">Net XXL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NHKWorldJapan.jp" site_id="1000102">NHK World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Nickelodeon.si" site_id="1000301">Nickelodeon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NickJr.si" site_id="1000426">Nick JR</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Nova24TV2.si" site_id="1000531">Nova 24 TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Nova24TV.si" site_id="1000432">Nova 24 TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NTVICKakanj.ba" site_id="1000235">NTV IC Kakanj</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OBN.ba" site_id="152">OBN</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OKanal.ba" site_id="1000186">O Kanal</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ORF1.at" site_id="66">ORF1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ORF2Europe.at" site_id="67">ORF2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OronTV.si" site_id="1000360">TV Oron</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OTO.si" site_id="1000365">OTO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OTV.hr" site_id="1000190">OTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OTVValentino.ba" site_id="1000073">OTV Valentino</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PeTV.si" site_id="1000101">PETV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkExtra.rs" site_id="1000095">Pink Extra</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkFilm.rs" site_id="1000096">Pink Film</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000094">Pink Folk</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkHits.rs" site_id="1000789">Pink Hits</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkMusic.rs" site_id="1000097">Pink Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkPlus.rs" site_id="107">Pink Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkReality.rs" site_id="1000351">Pink Reality</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkSI.rs" site_id="1000361">Pink SI</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkWorld.rs" site_id="1000352">Pink World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkZabava.rs" site_id="1000353">Pink Zabava</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlanetEva.si" site_id="1000494">Planet Eva</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlanetTV2.si" site_id="1000485">Planet TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlanetTV.si" site_id="1000278">Planet TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlayHouse.si" site_id="1000294">Play House</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="POPTV.si" site_id="1000371">POP TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ProSieben.de" site_id="69">Pro 7</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Prva.rs" site_id="1000210">Prva</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Rai1.it" site_id="1000625">RAI 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Rai2.it" site_id="1000626">RAI 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Rai3.it" site_id="1000627">RAI 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="REDxxx.si" site_id="1000490">RED xxx</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RT.ru" site_id="2000000028">RT</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTK1.xk" site_id="2000000015">RTK Kosova</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTL2.hr" site_id="1000322">RTL 2 HR</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTL.de" site_id="70">RTL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTL.hr" site_id="1000314">RTL Televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLKetto.hu" site_id="71">RTL2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLKockica.hr" site_id="1000355">RTL Kockica</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLLiving.de" site_id="1000323">RTL Living</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLSuper.de" site_id="73">Super RTL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTRSTV.ba" site_id="1000185">RTRS</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTS1.rs" site_id="1000211">RTS 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTS2.rs" site_id="1000212">RTS 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTSKlasika.rs" site_id="108">RTS</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTVi.ru" site_id="1000666">RTVi</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTVVikom.ba" site_id="1000188">Vikom TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SAT1.de" site_id="72">SAT1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SciFi.rs" site_id="1000614">Sci Fi</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ServusTV.at" site_id="1000086">Servus TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SexationTV.si" site_id="1000408">Sexation</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SIPTV.si" site_id="1000498">SIP TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Sitel.mk" site_id="1000201">TV Sitel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SkyNewsInternational.uk" site_id="2000000002">Sky News</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Sport1.de" site_id="60">SPORT1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SportTV1.si" site_id="1000338">Šport TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SportTV2.si" site_id="1000339">Šport TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SportTV3.si" site_id="1000384">Šport TV 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="StingrayFestival4K.ca" site_id="1000527">Festival</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SuperONE.hu@HD" site_id="1000342">Super One</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="T2Info.si" site_id="1000543">T-2 Info</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Telecafe.ru" site_id="1000456">Telecafe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TelmaTV.mk" site_id="1000226">Telma TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TLCBalkan.us" site_id="1000763">TLC</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TlnovelasEuropa.mx" site_id="100">TL Novelas Europe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TNTMusic.ru" site_id="1000664">TNT Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TopTV.si" site_id="1000356">TOP TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ToxicTV.rs" site_id="1000684">TV Toxic</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TraceSportStars.fr" site_id="1000169">Trace Sport Stars</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TraceUrban.fr" site_id="47">Trace Urban</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TravelChannelEMEA.uk" site_id="1000168">Travel Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000511">Travelxp 4K</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Travelxp.in" site_id="1000500">Travelxp</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TringAction.al" site_id="1000227">Tring Action</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TringShqip.al" site_id="1000216">Tring Shqip</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TringTring.al" site_id="1000217">Tring Tring</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TrzicTV.si" site_id="1000620">Tržič TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TV3.si" site_id="1000510">TV 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000405">TV 8</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TV24.mk" site_id="1000772">24 Vesti</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVArena.si" site_id="1000529">Arena TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="122">TV AS</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVCelje.si" site_id="1000065">TV Celje</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVCGMNE.me" site_id="109">MNE</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVDugaPlus.rs" site_id="1000035">TV Duga Novi Sad</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVEInternacionalEuropeAsia.es" site_id="98">TVE</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVGaleja.si" site_id="1000343">TV Galeja</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVIDEA.si" site_id="123">TV IDEA</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVJadran.hr" site_id="1000194">TV Jadran</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKCN1.rs" site_id="1000078">KCN</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKCN2.rs" site_id="1000103">KCN Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKCN3.rs" site_id="2000000046">KCN 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKoperCapodistria.si" site_id="1000624">TV Koper</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVMaribor.si" site_id="1000623">TV Maribor</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVMiklavz.si" site_id="1000727">TV Miklavž</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSA.ba" site_id="1000187">TV Sarajevo</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSLO1.si" site_id="1000259">SLO 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSLO2.si" site_id="1000260">SLO 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSLO3.si" site_id="1000555">SLO 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSlonExtra.ba" site_id="1000776">TV Slon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVVijesti.me" site_id="1000775">Vijesti</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Vaskanal.si" site_id="1000002">Vaš kanal</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VeseljakTV.si" site_id="13">Best TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ViasatExplore.se" site_id="1000045">Viasat Explore</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ViasatHistory.se" site_id="1000046">Viasat History</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ViasatNature.se" site_id="1000081">Viasat Nature</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="vijuTV1000.ru" site_id="1000010">TV1000</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Vitel.si" site_id="2000000048">Vitel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VividRedHD.us" site_id="1000508">Vivid Red</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VividTVEurope.uk" site_id="1000489">Vivid TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VizionPlus.al" site_id="1000079">Tring Vizion+</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VOX.de" site_id="78">VOX</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Vremya.ru" site_id="1000276">Vremya</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VTV.si" site_id="2000000044">VTV Velenje</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="vZIVOsi.si" site_id="2000000043">vŽivo.si</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Z1.hr" site_id="151">Z1 televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ZDF.de" site_id="1000064">ZDF</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ZdravaTV7.hr" site_id="1000266">Zdrava Televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ZdravaTV.si" site_id="1000616">Zdrava TV</channel>
</channels>
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="14">RTS Maribor</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="17">TV Veseljak Golica</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="26">Discovery Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="110">Hayat Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000028">AMC</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000043">History Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000048">History Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000074">Disney Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000080">Folk Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000155">Disney Junior</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000181">Alfa TV MAK</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000223">MTV 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000224">Naša TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000267">Alfa TV BiH</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000283">INFO TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000291">Animal Planet</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000302">Nickelodeon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000470">TV Nakupi</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000477">HBO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000478">HBO 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000479">HBO 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000480">Cinemax</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000481">Cinemax 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000503">RT Doc</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000505">Discovery Science</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000506">DTX</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000507">ID</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000517">Freedom</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000547">Filmbox Premium</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000615">E!</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000765">Pink Serije</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000766">Pink Koncert</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000767">PinknRoll</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000792">Sonce TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000793">Prva World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000794">Prva Max</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000795">Happy Reality</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000796">Happy Reality 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000797">Prva Files</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000798">Prva Kick</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000799">Prva Life</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000800">Prva Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000801">Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000802">One Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000803">Folx Slovenija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000807">BBC News</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000808">Dom TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000812">Espreso TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000813">Duck TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000814">Non Stop</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000815">Hit TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000816">Bloomberg Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000817">Arena Sport 1 Premium</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000818">Megafon TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000820">CineStar TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000821">LH TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000825">English Club TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000826">Harmonika TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000827">GLAM</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000828">XXXTazy</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000829">Angels</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000830">BooB</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000831">Capable Hole</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000832">Devils Home</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000833">Foxy Dolls</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000834">MIxxx</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000835">Prva TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000868">BIR TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000870">KIC TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000871">Kitchen TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000874">Mediaset Italia</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000875">TgCom24</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000001">R Kanal+</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000039">Cartoon Network</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000041">RTV Shqiptar</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000045">Europe by Satellite</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="2000000050">RTV Atlas</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="3sat.de" site_id="1000640">3SAT</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="24Kitchen.us@Slovenia" site_id="1000307">24Kitchen Adria</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="360TuneBox.nl" site_id="1000558">360 Tunebox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AgroTV.rs" site_id="1000686">Agro TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AlJazeeraBalkans.ba" site_id="1000180">Al Jazeera Balkans</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Alsat.mk" site_id="1000219">Alsat Macedoniae</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AMCEurope.uk@Portugal@Slovenia" site_id="1000523">AMC</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="AnixeHDSerie.de" site_id="1000057">Anixe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaEsport.rs" site_id="1000683">TV Arena Esport</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaFight.rs" site_id="1000777">Arena Fight</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport1.rs" site_id="1000688">Arena Sport 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport2.rs" site_id="1000689">Arena Sport 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport3.rs" site_id="1000787">Arena Sport 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ArenaSport4.rs" site_id="1000788">Arena Sport 4</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="arte.fr" site_id="1000026">Arte</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ATMTV.si" site_id="1000001">ATM Kranjska Gora</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="B92.rs" site_id="1000207">B92</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BabyTV.uk" site_id="82">Baby TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BalkanErotic.si" site_id="1000645">Balkan Erotic</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BalkanikaTV.bg" site_id="2000000027">Balkanika Music TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BalkanTripTV.rs" site_id="1000685">TV Balkan Trip</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BBCEarth.uk@Romania" site_id="1000482">BBC Earth</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BBCFirst.uk@Poland" site_id="1000652">BBC First</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BHT1.ba" site_id="1000182">BHT 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BKTV.si" site_id="1000265">BK TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BN2.ba" site_id="129">BN TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BNMusic.ba" site_id="1000067">BN Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CartoonitoCEE.uk" site_id="2000000032">Cartoonito</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BoomTV.si" site_id="124">Aktual TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="BRIO.si" site_id="1000368">BRIO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Carousel.ru" site_id="1000273">Karousel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CBSRealityEMEA.uk" site_id="44">CBS Reality</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CGTN.cn" site_id="125">CGTN</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ChannelOne.ru" site_id="1000272">Channel One Russia</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTV1.rs" site_id="1000085">CineStar TV 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVAction.rs" site_id="1000427">Cinestar Action &amp; Thriller</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVComedy.hr" site_id="1000617">Cinestar Comedy</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVFantasy.hr" site_id="1000618">Cinestar Fantasy</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVPremiere1.hr" site_id="1000431">Cinestar Premiere</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CineStarTVPremiere2.hr" site_id="1000430">Cinestar Premiere 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ClubMTVEurope.uk" site_id="126">Club MTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CMCTV.hr" site_id="1000156">CMC - Croatian Music Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CNNInternational.us@MENA" site_id="25">CNN International</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="CrimePlusInvestigation.uk" site_id="1000153">Crime &amp; Investigation Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DasErste.de" site_id="1000059">Das Erste (ARD)</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DaVinci.de" site_id="1000055">Da Vinci</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DivaAdria.us" site_id="2000000040">Diva</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DMSat.rs" site_id="2000000026">DM SAT Televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DocuBox.nl" site_id="1000551">Docubox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Domkino.ru" site_id="1000274">Dom Kino</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DorcelXXX.nl" site_id="1000486">Dorcel TV XXX</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Duna.hu" site_id="1000648">Duna</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DunaWorld.hu" site_id="1000786">Duna World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="DuskTV.nl" site_id="1000373">Dusk</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ELTA2.ba" site_id="1000179">Elta 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ELTA1HD.ba" site_id="1000240">Elta TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EpicDrama.uk@Sweden" site_id="1000501">Epic Drama</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ePosavjeTV.si" site_id="1000651">ePosavje TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EroX.nl" site_id="1000559">Erox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EroXXX.nl" site_id="1000553">Eroxxx</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ETV.si" site_id="1000641">ETV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EuronewsEnglish.fr" site_id="1000764">Euronews</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport1.fr@Germany" site_id="1000044">Eurosport (NEM)</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport1.fr" site_id="1000025">Eurosport</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport2.fr" site_id="1000504">Eurosport 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Eurosport4K.fr" site_id="1000728">Eurosport</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="EWTN.us@Europe" site_id="36">EWTN Europe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ExodusTV.si" site_id="1000455">Exodus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Extreme.si" site_id="1000646">Extreme</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ExtremeSportsChannel.nl" site_id="37">Extreme Sports</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FashionBox.nl" site_id="1000556">Fashionbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FashionTVEurope.fr" site_id="1000056">Fashion TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FastFunBox.nl" site_id="1000552">Fastnfunbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Federalnatelevizija.ba" site_id="1000183">FTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FenFolkTV.bg" site_id="1000545">FenFolk TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FenTV.bg" site_id="1000544">FEN TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FightBox.nl" site_id="1000550">Fightbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FilmBoxArthouse.nl" site_id="1000557">Filmbox Art House</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FilmBoxExtra.nl@Bulgaria" site_id="1000548">Filmbox Extra</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FilmBoxStars.nl@Bulgaria" site_id="1000549">Filmbox Stars</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Fox.rs" site_id="1000308">STAR</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FoxCrime.rs" site_id="1000310">STAR Crime</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FoxLife.rs" site_id="1000309">STAR Life</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FoxMovies.si" site_id="1000311">STAR Movies</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="France2.fr" site_id="1000639">FR2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="France24.fr@English" site_id="1000159">France 24 English</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="France24.fr@French" site_id="1000158">France 24 French</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="FunBoxUHD.nl" site_id="1000521">Funbox</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Gametoon.nl" site_id="1000554">Gametoon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="GeaTV.si" site_id="1000034">Gea TV Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="GOLDTV.si" site_id="1000407">GOLD TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="GolicaTV.si" site_id="2000000056">TV Zlati zvoki</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Happy.rs" site_id="1000209">Happy TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Hayat.ba" site_id="1000236">Hayat</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HayatFolk.ba" site_id="1000284">Hayat Folk</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HemaTV.ba" site_id="1000233">Hema</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HGTVLatinAmerica.us@Panregional" site_id="1000730">HGTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="History2.pl" site_id="1000619">H2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HotPleasure.si" site_id="1000644">Hot Pleasure</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HotXXL.si" site_id="1000643">Hot XXL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HRT1.hr" site_id="105">HRT 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HRT2.hr" site_id="106">HRT 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HustlerHD.nl" site_id="1000306">Hustler TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="HustlerTVEurope.nl" site_id="1000018">Hustler TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="JimJamEurope.uk" site_id="1000509">Jim Jam</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000285">Jugoton TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="kabeleins.de" site_id="61">Kabel 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Kanal5.mk" site_id="1000268">Kanal 5</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="KanalA.si" site_id="1000370">Kanal A</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Kanali7.al" site_id="1000174">Tring 7</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="KINO.si" site_id="1000366">KINO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Klasik.hr" site_id="1000154">Klasik</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="KoroskaTV.si" site_id="1000474">Koroška TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="M1.hu" site_id="1000647">M1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="M2.hu" site_id="1000050">M2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="M5.hu" site_id="1000650">M5</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Mezzo.fr" site_id="91">Mezzo</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MezzoLive.fr" site_id="1000518">Mezzo Live</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MilfTV.si" site_id="1000491">Milf TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MinimaxCEE.cz" site_id="1000262">Minimax</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MrezaTV.hr" site_id="1000193">Mreža TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MRT1.mk" site_id="1000222">MTV 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MRT2.mk" site_id="1000199">MTV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTV00s.uk" site_id="50">MTV 00s</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTV80s.uk" site_id="51">MTV 80s</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTV90s.uk" site_id="136">MTV 90s</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTVGlobal.uk" site_id="1000496">MTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTVHitsEurope.uk" site_id="127">MTV Hits</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MTVLive.uk" site_id="1000497">MTV Live</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="MuzykaPervogo.ru" site_id="1000275">Muzika Pervogo</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NarodnaTV.rs" site_id="1000269">Narodna TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NationalGeographic.si" site_id="1000049">National Geographic</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NationalGeographicWild.si" site_id="1000167">National Geographic Wild</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NetTV.si" site_id="12">Net TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NetXXL.si" site_id="1000228">Net XXL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NHKWorldJapan.jp" site_id="1000102">NHK World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Nickelodeon.si" site_id="1000301">Nickelodeon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NickJr.si" site_id="1000426">Nick JR</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Nova24TV2.si" site_id="1000531">Nova 24 TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Nova24TV.si" site_id="1000432">Nova 24 TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="NTVICKakanj.ba" site_id="1000235">NTV IC Kakanj</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OBN.ba" site_id="152">OBN</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OKanal.ba" site_id="1000186">O Kanal</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ORF1.at" site_id="66">ORF1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ORF2Europe.at" site_id="67">ORF2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OronTV.si" site_id="1000360">TV Oron</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OTO.si" site_id="1000365">OTO</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OTV.hr" site_id="1000190">OTV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="OTVValentino.ba" site_id="1000073">OTV Valentino</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PeTV.si" site_id="1000101">PETV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkExtra.rs" site_id="1000095">Pink Extra</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkFilm.rs" site_id="1000096">Pink Film</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000094">Pink Folk</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkHits.rs" site_id="1000789">Pink Hits</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkMusic.rs" site_id="1000097">Pink Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkPlus.rs" site_id="107">Pink Plus</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkReality.rs" site_id="1000351">Pink Reality</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkSI.rs" site_id="1000361">Pink SI</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkWorld.rs" site_id="1000352">Pink World</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PinkZabava.rs" site_id="1000353">Pink Zabava</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlanetEva.si" site_id="1000494">Planet Eva</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlanetTV2.si" site_id="1000485">Planet TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlanetTV.si" site_id="1000278">Planet TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="PlayHouse.si" site_id="1000294">Play House</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="POPTV.si" site_id="1000371">POP TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ProSieben.de" site_id="69">Pro 7</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Prva.rs" site_id="1000210">Prva</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Rai1.it" site_id="1000625">RAI 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Rai2.it" site_id="1000626">RAI 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Rai3.it" site_id="1000627">RAI 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="REDxxx.si" site_id="1000490">RED xxx</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RT.ru" site_id="2000000028">RT</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTK1.xk" site_id="2000000015">RTK Kosova</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTL2.hr" site_id="1000322">RTL 2 HR</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTL.de" site_id="70">RTL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTL.hr" site_id="1000314">RTL Televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLKetto.hu" site_id="71">RTL2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLKockica.hr" site_id="1000355">RTL Kockica</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLLiving.de" site_id="1000323">RTL Living</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTLSuper.de" site_id="73">Super RTL</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTRSTV.ba" site_id="1000185">RTRS</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTS1.rs" site_id="1000211">RTS 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTS2.rs" site_id="1000212">RTS 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTSKlasika.rs" site_id="108">RTS</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTVi.ru" site_id="1000666">RTVi</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="RTVVikom.ba" site_id="1000188">Vikom TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SAT1.de" site_id="72">SAT1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SciFi.rs" site_id="1000614">Sci Fi</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ServusTV.at" site_id="1000086">Servus TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SexationTV.si" site_id="1000408">Sexation</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SIPTV.si" site_id="1000498">SIP TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Sitel.mk" site_id="1000201">TV Sitel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SkyNewsInternational.uk" site_id="2000000002">Sky News</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Sport1.de" site_id="60">SPORT1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SportTV1.si" site_id="1000338">Šport TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SportTV2.si" site_id="1000339">Šport TV 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SportTV3.si" site_id="1000384">Šport TV 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="StingrayFestival4K.ca" site_id="1000527">Festival</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="SuperONE.hu@HD" site_id="1000342">Super One</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="T2Info.si" site_id="1000543">T-2 Info</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Telecafe.ru" site_id="1000456">Telecafe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TelmaTV.mk" site_id="1000226">Telma TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TLCBalkan.us" site_id="1000763">TLC</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TlnovelasEuropa.mx" site_id="100">TL Novelas Europe</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TNTMusic.ru" site_id="1000664">TNT Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TopTV.si" site_id="1000356">TOP TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ToxicTV.rs" site_id="1000684">TV Toxic</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TraceSportStars.fr" site_id="1000169">Trace Sport Stars</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TraceUrban.fr" site_id="47">Trace Urban</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TravelChannelEMEA.uk" site_id="1000168">Travel Channel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000511">Travelxp 4K</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Travelxp.in" site_id="1000500">Travelxp</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TringAction.al" site_id="1000227">Tring Action</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TringShqip.al" site_id="1000216">Tring Shqip</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TringTring.al" site_id="1000217">Tring Tring</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TrzicTV.si" site_id="1000620">Tržič TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TV3.si" site_id="1000510">TV 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="1000405">TV 8</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TV24.mk" site_id="1000772">24 Vesti</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVArena.si" site_id="1000529">Arena TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="" site_id="122">TV AS</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVCelje.si" site_id="1000065">TV Celje</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVCGMNE.me" site_id="109">MNE</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVDugaPlus.rs" site_id="1000035">TV Duga Novi Sad</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVEInternacionalEuropeAsia.es" site_id="98">TVE</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVGaleja.si" site_id="1000343">TV Galeja</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVIDEA.si" site_id="123">TV IDEA</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVJadran.hr" site_id="1000194">TV Jadran</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKCN1.rs" site_id="1000078">KCN</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKCN2.rs" site_id="1000103">KCN Music</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKCN3.rs" site_id="2000000046">KCN 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVKoperCapodistria.si" site_id="1000624">TV Koper</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVMaribor.si" site_id="1000623">TV Maribor</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVMiklavz.si" site_id="1000727">TV Miklavž</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSA.ba" site_id="1000187">TV Sarajevo</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSLO1.si" site_id="1000259">SLO 1</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSLO2.si" site_id="1000260">SLO 2</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSLO3.si" site_id="1000555">SLO 3</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVSlonExtra.ba" site_id="1000776">TV Slon</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="TVVijesti.me" site_id="1000775">Vijesti</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Vaskanal.si" site_id="1000002">Vaš kanal</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VeseljakTV.si" site_id="13">Best TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ViasatExplore.se" site_id="1000045">Viasat Explore</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ViasatHistory.se" site_id="1000046">Viasat History</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ViasatNature.se" site_id="1000081">Viasat Nature</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="vijuTV1000.ru" site_id="1000010">TV1000</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Vitel.si" site_id="2000000048">Vitel</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VividRedHD.us" site_id="1000508">Vivid Red</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VividTVEurope.uk" site_id="1000489">Vivid TV</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VizionPlus.al" site_id="1000079">Tring Vizion+</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VOX.de" site_id="78">VOX</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Vremya.ru" site_id="1000276">Vremya</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="VTV.si" site_id="2000000044">VTV Velenje</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="vZIVOsi.si" site_id="2000000043">vŽivo.si</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="Z1.hr" site_id="151">Z1 televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ZDF.de" site_id="1000064">ZDF</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ZdravaTV7.hr" site_id="1000266">Zdrava Televizija</channel>
<channel site="tv2go.t-2.net" lang="sl" xmltv_id="ZdravaTV.si" site_id="1000616">Zdrava TV</channel>
</channels>

View File

@@ -1,68 +1,68 @@
const { parser, url, request } = require('./tv2go.t-2.net.config.js')
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)
const date = dayjs.utc('2021-11-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1000259',
xmltv_id: 'TVSlovenija1.si'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg'
)
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/json'
})
})
it('can generate valid request data', () => {
expect(request.data({ date, channel })).toMatchObject({
locale: 'sl-SI',
channelId: [1000259],
startTime: 1637280000000,
endTime: 1637366400000,
imageInfo: [{ height: 500, width: 1100 }],
includeBookmarks: false,
includeShow: true
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json'), 'utf8')
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-19T00:50:00.000Z',
stop: '2021-11-19T01:15:00.000Z',
title: 'Dnevnik Slovencev v Italiji',
category: ['Informativni'],
description:
'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.',
image: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: 'Invalid API client identifier'
})
expect(result).toMatchObject([])
})
const { parser, url, request } = require('./tv2go.t-2.net.config.js')
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)
const date = dayjs.utc('2021-11-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1000259',
xmltv_id: 'TVSlovenija1.si'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg'
)
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/json'
})
})
it('can generate valid request data', () => {
expect(request.data({ date, channel })).toMatchObject({
locale: 'sl-SI',
channelId: [1000259],
startTime: 1637280000000,
endTime: 1637366400000,
imageInfo: [{ height: 500, width: 1100 }],
includeBookmarks: false,
includeShow: true
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.json'), 'utf8')
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-19T00:50:00.000Z',
stop: '2021-11-19T01:15:00.000Z',
title: 'Dnevnik Slovencev v Italiji',
category: ['Informativni'],
description:
'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.',
image: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: 'Invalid API client identifier'
})
expect(result).toMatchObject([])
})

View File

@@ -1,99 +1,99 @@
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 uniqBy = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'tvcesoir.fr',
days: 2,
url: function ({ date, channel }) {
return `https://www.tvcesoir.fr/programme-tv/programme/chaine/${
channel.site_id
}.html?dt=${date.format('YYYY-MM-DD')}`
},
parser: function ({ content, date, channel }) {
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, channel)
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
},
async channels() {
const axios = require('axios')
const providers = ['-1', '-2', '-3', '-4', '-5']
const channels = []
for (let provider of providers) {
const data = await axios
.post('https://www.tvcesoir.fr/guide/schedule', null, {
params: {
provider,
region: 'France',
TVperiod: 'Night',
date: dayjs().format('YYYY-MM-DD'),
st: 0,
u_time: 2155,
is_mobile: 1
}
})
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
$('.channelname').each((i, el) => {
const name = $(el).find('center > a:eq(1)').text()
const url = $(el).find('center > a:eq(1)').attr('href')
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
channels.push({
lang: 'fr',
name,
site_id: `${number}/${slug}`
})
})
}
return uniqBy(channels, x => x.site_id)
}
}
function parseStart($item, date) {
const timeString = $item('td:eq(0)').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH[h]mm', 'Europe/Rome')
}
function parseTitle($item) {
return $item('td:eq(1)').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('table.table > 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')
const uniqBy = require('../../scripts/functions')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'tvcesoir.fr',
days: 2,
url: function ({ date, channel }) {
return `https://www.tvcesoir.fr/programme-tv/programme/chaine/${
channel.site_id
}.html?dt=${date.format('YYYY-MM-DD')}`
},
parser: function ({ content, date, channel }) {
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, channel)
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
},
async channels() {
const axios = require('axios')
const providers = ['-1', '-2', '-3', '-4', '-5']
const channels = []
for (let provider of providers) {
const data = await axios
.post('https://www.tvcesoir.fr/guide/schedule', null, {
params: {
provider,
region: 'France',
TVperiod: 'Night',
date: dayjs().format('YYYY-MM-DD'),
st: 0,
u_time: 2155,
is_mobile: 1
}
})
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(data)
$('.channelname').each((i, el) => {
const name = $(el).find('center > a:eq(1)').text()
const url = $(el).find('center > a:eq(1)').attr('href')
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
channels.push({
lang: 'fr',
name,
site_id: `${number}/${slug}`
})
})
}
return uniqBy(channels, x => x.site_id)
}
}
function parseStart($item, date) {
const timeString = $item('td:eq(0)').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH[h]mm', 'Europe/Rome')
}
function parseTitle($item) {
return $item('td:eq(1)').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('table.table > tbody > tr').toArray()
}

View File

@@ -1,50 +1,50 @@
const { parser, url } = require('./tvcesoir.fr.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-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '847049/tf-1',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.tvcesoir.fr/programme-tv/programme/chaine/847049/tf-1.html?dt=2023-11-24'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__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[0]).toMatchObject({
start: '2023-11-24T01:00:00.000Z',
stop: '2023-11-24T01:10:00.000Z',
title: "Tirage de l'Euro Millions"
})
expect(results[26]).toMatchObject({
start: '2023-11-24T22:45:00.000Z',
stop: '2023-11-24T23:15:00.000Z',
title: 'Juge Arthur'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tvcesoir.fr.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-11-24', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '847049/tf-1',
xmltv_id: 'TF1.fr'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.tvcesoir.fr/programme-tv/programme/chaine/847049/tf-1.html?dt=2023-11-24'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__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[0]).toMatchObject({
start: '2023-11-24T01:00:00.000Z',
stop: '2023-11-24T01:10:00.000Z',
title: "Tirage de l'Euro Millions"
})
expect(results[26]).toMatchObject({
start: '2023-11-24T22:45:00.000Z',
stop: '2023-11-24T23:15:00.000Z',
title: 'Juge Arthur'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,52 +1,52 @@
const { parser, url } = require('./tvcubana.icrt.cu.config.js')
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)
const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'cv',
xmltv_id: 'CubavisionNacional.cu'
}
let content = fs.readFileSync(path.resolve(__dirname, './__data__/content.json'), {encoding: 'utf8'})
// in the specific case of this site, the unicode escape sequences are double-escaped
content = content.replace(/\\\\u([0-9a-fA-F]{4})/g, '\\u$1')
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php')
})
it('can generate valid url for next day', () => {
expect(url({ channel, date: date.add(2, 'd') })).toBe(
'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php'
)
})
it('can parse response', () => {
const result = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-22T05:40:00.000Z',
stop: '2021-11-22T05:50:00.000Z',
title: 'CARIBE NOTICIAS',
description: 'EMISIÓN DE CIERRE.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tvcubana.icrt.cu.config.js')
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)
const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'cv',
xmltv_id: 'CubavisionNacional.cu'
}
let content = fs.readFileSync(path.resolve(__dirname, './__data__/content.json'), {encoding: 'utf8'})
// in the specific case of this site, the unicode escape sequences are double-escaped
content = content.replace(/\\\\u([0-9a-fA-F]{4})/g, '\\u$1')
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php')
})
it('can generate valid url for next day', () => {
expect(url({ channel, date: date.add(2, 'd') })).toBe(
'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php'
)
})
it('can parse response', () => {
const result = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-22T05:40:00.000Z',
stop: '2021-11-22T05:50:00.000Z',
title: 'CARIBE NOTICIAS',
description: 'EMISIÓN DE CIERRE.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./tvguide.myjcom.jp.config.js')
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)
const date = dayjs.utc('2022-01-14', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '120_200_4',
name: 'Star Channel 1',
xmltv_id: 'StarChannel1.jp'
}
const content = fs.readFileSync(path.resolve(__dirname, './__data__/content.json'), 'utf8')
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114')
})
it('can parse response', () => {
const result = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-13T20:00:00.000Z',
stop: '2022-01-13T21:00:00.000Z',
title: '[5.1]フードロア:タマリンド',
description:
'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)',
image:
'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg',
category: 'ドラマ'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.json'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tvguide.myjcom.jp.config.js')
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)
const date = dayjs.utc('2022-01-14', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '120_200_4',
name: 'Star Channel 1',
xmltv_id: 'StarChannel1.jp'
}
const content = fs.readFileSync(path.resolve(__dirname, './__data__/content.json'), 'utf8')
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114')
})
it('can parse response', () => {
const result = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-01-13T20:00:00.000Z',
stop: '2022-01-13T21:00:00.000Z',
title: '[5.1]フードロア:タマリンド',
description:
'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)',
image:
'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg',
category: 'ドラマ'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, './__data__/no_content.json'), 'utf8')
})
expect(result).toMatchObject([])
})

View File

@@ -1,97 +1,97 @@
const cheerio = require('cheerio')
const axios = require('axios')
const { DateTime } = require('luxon')
const { uniqBy } = require('../../scripts/functions')
module.exports = {
site: 'tvhebdo.com',
days: 2,
url: function ({ channel, date }) {
return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format(
'YYYY-MM-DD'
)}`
},
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 (prev) {
if (start < prev.start) {
start = start.plus({ days: 1 })
}
prev.stop = start
}
let stop = start.plus({ minutes: 30 })
programs.push({
title: parseTitle($item),
start,
stop
})
})
return programs
},
async channels() {
let items = []
const offsets = [
0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360
]
for (let offset of offsets) {
const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00`
console.log(url)
const html = await axios
.get(url, {
headers: {
Cookie:
'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006'
}
})
.then(r => r.data)
.catch(console.error)
const $ = cheerio.load(html)
const rows = $('table.gr_row').toArray()
items = items.concat(rows)
}
let channels = []
items.forEach(item => {
const $item = cheerio.load(item)
const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr(
'alt'
)
const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href')
const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null]
channels.push({
lang: 'fr',
site_id,
name
})
})
return uniqBy(channels, x => x.site_id)
}
}
function parseTitle($item) {
return $item('.titre').first().text().trim()
}
function parseStart($item, date) {
const time = $item('.heure').text()
return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'America/Toronto'
}).toUTC()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $(
'#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]'
).toArray()
}
const cheerio = require('cheerio')
const axios = require('axios')
const { DateTime } = require('luxon')
const { uniqBy } = require('../../scripts/functions')
module.exports = {
site: 'tvhebdo.com',
days: 2,
url: function ({ channel, date }) {
return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format(
'YYYY-MM-DD'
)}`
},
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 (prev) {
if (start < prev.start) {
start = start.plus({ days: 1 })
}
prev.stop = start
}
let stop = start.plus({ minutes: 30 })
programs.push({
title: parseTitle($item),
start,
stop
})
})
return programs
},
async channels() {
let items = []
const offsets = [
0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360
]
for (let offset of offsets) {
const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00`
console.log(url)
const html = await axios
.get(url, {
headers: {
Cookie:
'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006'
}
})
.then(r => r.data)
.catch(console.error)
const $ = cheerio.load(html)
const rows = $('table.gr_row').toArray()
items = items.concat(rows)
}
let channels = []
items.forEach(item => {
const $item = cheerio.load(item)
const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr(
'alt'
)
const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href')
const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null]
channels.push({
lang: 'fr',
site_id,
name
})
})
return uniqBy(channels, x => x.site_id)
}
}
function parseTitle($item) {
return $item('.titre').first().text().trim()
}
function parseStart($item, date) {
const time = $item('.heure').text()
return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'America/Toronto'
}).toUTC()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $(
'#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]'
).toArray()
}

View File

@@ -1,45 +1,45 @@
const { parser, url } = require('./tvheute.at.config.js')
const dayjs = require('dayjs')
const path = require('path')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const { readFileSync } = require('fs')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021'
)
})
it('can parse response', () => {
expect(parser({ date, channel, content: readFileSync(path.resolve(__dirname, './__data__/content.html'), 'utf8') })).toMatchObject([
{
start: '2021-11-08T05:00:00.000Z',
stop: '2021-11-08T05:10:00.000Z',
title: 'Monchhichi (Wh.)',
category: 'Kids',
description:
'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.',
image: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg'
},
{
start: '2021-11-08T17:00:00.000Z',
stop: '2021-11-08T17:10:00.000Z',
title: 'ZIB 18'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
const { parser, url } = require('./tvheute.at.config.js')
const dayjs = require('dayjs')
const path = require('path')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
const { readFileSync } = require('fs')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021'
)
})
it('can parse response', () => {
expect(parser({ date, channel, content: readFileSync(path.resolve(__dirname, './__data__/content.html'), 'utf8') })).toMatchObject([
{
start: '2021-11-08T05:00:00.000Z',
stop: '2021-11-08T05:10:00.000Z',
title: 'Monchhichi (Wh.)',
category: 'Kids',
description:
'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.',
image: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg'
},
{
start: '2021-11-08T17:00:00.000Z',
stop: '2021-11-08T17:10:00.000Z',
title: 'ZIB 18'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})