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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./chaines-tv.orange.fr.config.js') const { parser, url } = require('./chaines-tv.orange.fr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '192', site_id: '192',
xmltv_id: 'TF1.fr' xmltv_id: 'TF1.fr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ channel, date }) const result = url({ channel, date })
expect(result).toBe( 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' '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', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content }) const result = parser({ date, channel, content })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-07T23:35:00.000Z', start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z', stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste', title: 'Tête de liste',
subTitle: 'Esprits criminels', subTitle: 'Esprits criminels',
season: 10, season: 10,
episode: 12, episode: 12,
description: 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.", "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', category: 'Série Suspense',
image: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' image: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

@@ -1,47 +1,47 @@
const { url, parser } = require('./cubmu.com.config.js') const { url, parser } = require('./cubmu.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d') const date = dayjs.utc('2023-11-05', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' } const channel = { site_id: '4028c68574537fcd0174be43042758d8', xmltv_id: 'TransTV.id', lang: 'id' }
const channelEn = Object.assign({}, channel, { lang: 'en' }) const channelEn = Object.assign({}, channel, { lang: 'en' })
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( 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' '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', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const idResults = parser({ content, channel }) const idResults = parser({ content, channel })
expect(idResults).toMatchObject([ expect(idResults).toMatchObject([
{ {
start: '2023-11-04T18:30:00.000Z', start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z', stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News', title: 'CNN Tech News',
description: description:
'CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.' '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 }) const enResults = parser({ content, channel: channelEn })
expect(enResults).toMatchObject([ expect(enResults).toMatchObject([
{ {
start: '2023-11-04T18:30:00.000Z', start: '2023-11-04T18:30:00.000Z',
stop: '2023-11-04T19:00:00.000Z', stop: '2023-11-04T19:00:00.000Z',
title: 'CNN Tech News', title: 'CNN Tech News',
description: 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.' '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', () => { it('can handle empty guide', () => {
const results = parser({ content: '' }) const results = parser({ content: '' })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +1,43 @@
const { parser, url } = require('./foxsports.com.au.config.js') const { parser, url } = require('./foxsports.com.au.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '2', site_id: '2',
xmltv_id: 'FoxLeague.au' xmltv_id: 'FoxLeague.au'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe( expect(url({ date })).toBe(
'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15' 'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: 'NRL', title: 'NRL',
sub_title: 'Eels v Titans', sub_title: 'Eels v Titans',
description: 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.', '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', category: 'Rugby League',
start: '2022-12-13T13:00:00.000Z', start: '2022-12-13T13:00:00.000Z',
stop: '2022-12-13T14:00:00.000Z' stop: '2022-12-13T14:00:00.000Z'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({content: ''}, channel) const result = parser({content: ''}, channel)
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,51 +1,51 @@
const { parser, url } = require('./freetv.tv.config.js') const { parser, url } = require('./freetv.tv.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-03-28', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-03-28', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '3370462', site_id: '3370462',
xmltv_id: 'Kan11.il' xmltv_id: 'Kan11.il'
} }
it('can generate valid url', () => { 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') 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', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => { const results = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(2) expect(results.length).toBe(2)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'בוש 4 - פרק 3', title: 'בוש 4 - פרק 3',
description: 'עונה 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', 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', 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', start: '2025-03-27T21:26:00.000Z',
stop: '2025-03-27T22:17:00.000Z' stop: '2025-03-27T22:17:00.000Z'
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
title: 'אבא משתדל - 5. חבר', title: 'אבא משתדל - 5. חבר',
description: 'סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.', 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', 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', 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', start: '2025-03-27T22:17:00.000Z',
stop: '2025-03-27T22:43:00.000Z' stop: '2025-03-27T22:43:00.000Z'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: '' }) const results = parser({ content: '' })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

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

View File

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

View File

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

View File

@@ -1,52 +1,52 @@
const { parser, url } = require('./guidatv.sky.it.config.js') const { parser, url } = require('./guidatv.sky.it.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-05-06', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-05-06', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'DTH#10458', site_id: 'DTH#10458',
xmltv_id: '20Mediaset.it' xmltv_id: '20Mediaset.it'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( 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' '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', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => { const result = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-05-06T00:35:40.000Z', start: '2022-05-06T00:35:40.000Z',
stop: '2022-05-06T01:15:40.000Z', stop: '2022-05-06T01:15:40.000Z',
title: 'Distretto di Polizia', title: 'Distretto di Polizia',
description: 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.", "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, season: 6,
episode: 26, episode: 26,
image: image:
'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b', 'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b',
category: 'Intrattenimento/Fiction', category: 'Intrattenimento/Fiction',
url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9' url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

@@ -1,85 +1,85 @@
const { parser, url } = require('./guidetnt.com.config.js') const { parser, url } = require('./guidetnt.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
require('dayjs/locale/fr') require('dayjs/locale/fr')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'tf1', site_id: 'tf1',
xmltv_id: 'TF1.fr' xmltv_id: 'TF1.fr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1') expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1')
}) })
it('can parse response', async () => { it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
let results = await parser({ content, date }) let results = await parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(29) expect(results.length).toBe(29)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
category: 'Série', category: 'Série',
description: 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...", "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', start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z', stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis' title: 'Camping Paradis'
}) })
expect(results[2]).toMatchObject({ expect(results[2]).toMatchObject({
category: 'Magazine', category: 'Magazine',
description: 'Retrouvez tous vos programmes de nuit.', description: 'Retrouvez tous vos programmes de nuit.',
start: '2025-07-01T00:55:00.000Z', start: '2025-07-01T00:55:00.000Z',
stop: '2025-07-01T04:00:00.000Z', stop: '2025-07-01T04:00:00.000Z',
title: 'Programmes de la nuit' title: 'Programmes de la nuit'
}) })
expect(results[15]).toMatchObject({ expect(results[15]).toMatchObject({
category: 'Téléfilm', category: 'Téléfilm',
description: 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...", "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', start: '2025-07-01T12:25:00.000Z',
stop: '2025-07-01T14:00:00.000Z', stop: '2025-07-01T14:00:00.000Z',
title: "Trahie par l'amour" title: "Trahie par l'amour"
}) })
}) })
it('can parse response for current day', async () => { it('can parse response for current day', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) 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') }) let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(29) expect(results.length).toBe(29)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
category: 'Série', category: 'Série',
description: 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...", "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', start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z', stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis' title: 'Camping Paradis'
}) })
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
const results = await parser({ const results = await parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
}) })
expect(results).toEqual([]) expect(results).toEqual([])
}) })

View File

@@ -1,246 +1,246 @@
const { parser, url } = require('./horizon.tv.config.js') const { parser, url } = require('./horizon.tv.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2023-02-07', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-02-07', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '10024', site_id: '10024',
xmltv_id: 'AMCCzechRepublic.cz' xmltv_id: 'AMCCzechRepublic.cz'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1' 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1'
) )
}) })
it('can parse response', done => { it('can parse response', done => {
const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')) const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if ( if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2' url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'), 'utf8')) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'), 'utf8'))
}) })
} else if ( } else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3' url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json'), 'utf8')) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json'), 'utf8'))
}) })
} else if ( } else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4' url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_3.json'), 'utf8')) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_3.json'), 'utf8'))
}) })
} else if ( } else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78' url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_1.json'), 'utf8')) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_1.json'), 'utf8'))
}) })
} else if ( } else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b' url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_2.json'), 'utf8')) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_2.json'), 'utf8'))
}) })
} else if ( } else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad' url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_3.json'), 'utf8')) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_3.json'), 'utf8'))
}) })
} else if ( } else if (
url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7' url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_4.json'), 'utf8')) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content_listings_4.json'), 'utf8'))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content, channel, date }) parser({ content, channel, date })
.then(result => { .then(result => {
result = result.map(p => { result = result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2023-02-06T21:35:00.000Z', start: '2023-02-06T21:35:00.000Z',
stop: '2023-02-06T23:05:00.000Z', stop: '2023-02-06T23:05:00.000Z',
title: 'Avengement', title: 'Avengement',
description: 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.', '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'], category: ['Drama', 'Akcia'],
directors: ['Jesse V. Johnson'], directors: ['Jesse V. Johnson'],
actors: [ actors: [
'Scott Adkins', 'Scott Adkins',
'Craig Fairbrass', 'Craig Fairbrass',
'Thomas Turgoose', 'Thomas Turgoose',
'Nick Moran', 'Nick Moran',
'Kierston Wareing', 'Kierston Wareing',
'Leo Gregory', 'Leo Gregory',
'Mark Strange', 'Mark Strange',
'Luke LaFontaine', 'Luke LaFontaine',
'Beau Fowler', 'Beau Fowler',
'Dan Styles', 'Dan Styles',
'Christopher Sciueref', 'Christopher Sciueref',
'Matt Routledge', 'Matt Routledge',
'Jane Thorne', 'Jane Thorne',
'Louis Mandylor', 'Louis Mandylor',
'Terence Maynard', 'Terence Maynard',
'Greg Burridge', 'Greg Burridge',
'Michael Higgs', 'Michael Higgs',
'Damian Gallagher', 'Damian Gallagher',
'Daniel Adegboyega', 'Daniel Adegboyega',
'John Ioannou', 'John Ioannou',
'Sofie Golding-Spittle', 'Sofie Golding-Spittle',
'Joe Egan', 'Joe Egan',
'Darren Swain', 'Darren Swain',
'Lee Charles', 'Lee Charles',
'Dominic Kinnaird', 'Dominic Kinnaird',
"Ross O'Hennessy", "Ross O'Hennessy",
'Teresa Mahoney', 'Teresa Mahoney',
'Andrew Dunkelberger', 'Andrew Dunkelberger',
'Sam Hardy', 'Sam Hardy',
'Ivan Moy', 'Ivan Moy',
'Mark Sears', 'Mark Sears',
'Phillip Ray Tommy' 'Phillip Ray Tommy'
], ],
date: '2019' date: '2019'
}, },
{ {
start: '2023-02-07T04:35:00.000Z', start: '2023-02-07T04:35:00.000Z',
stop: '2023-02-07T05:00:00.000Z', stop: '2023-02-07T05:00:00.000Z',
title: 'Zoom In', title: 'Zoom In',
description: 'Film/Kino', description: 'Film/Kino',
category: ['Hudba a umenie', 'Film'], category: ['Hudba a umenie', 'Film'],
date: '2010' date: '2010'
}, },
{ {
start: '2023-02-07T09:10:00.000Z', start: '2023-02-07T09:10:00.000Z',
stop: '2023-02-07T11:00:00.000Z', stop: '2023-02-07T11:00:00.000Z',
title: 'Studentka', title: 'Studentka',
description: 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?', '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'], category: ['Film', 'Komédia'],
actors: [ actors: [
'Sophie Marceauová', 'Sophie Marceauová',
'Vincent Lindon', 'Vincent Lindon',
'Elisabeth Vitali', 'Elisabeth Vitali',
'Elena Pompei', 'Elena Pompei',
'Jean-Claude Leguay', 'Jean-Claude Leguay',
'Brigitte Chamarande', 'Brigitte Chamarande',
'Christian Pereira', 'Christian Pereira',
'Gérard Dacier', 'Gérard Dacier',
'Roberto Attias', 'Roberto Attias',
'Beppe Chierici', 'Beppe Chierici',
'Nathalie Mann', 'Nathalie Mann',
'Anne Macina', 'Anne Macina',
'Janine Souchon', 'Janine Souchon',
'Virginie Demians', 'Virginie Demians',
'Hugues Leforestier', 'Hugues Leforestier',
'Jacqueline Noëlle', 'Jacqueline Noëlle',
'Marc-André Brunet', 'Marc-André Brunet',
'Isabelle Caubère', 'Isabelle Caubère',
'André Chazel', 'André Chazel',
'Med Salah Cheurfi', 'Med Salah Cheurfi',
'Guillaume Corea', 'Guillaume Corea',
'Eric Denize', 'Eric Denize',
'Gilles Gaston-Dreyfuss', 'Gilles Gaston-Dreyfuss',
'Benoît Gourley', 'Benoît Gourley',
'Marc Innocenti', 'Marc Innocenti',
'Najim Laouriga', 'Najim Laouriga',
'Laurent Ledermann', 'Laurent Ledermann',
'Philippe Maygal', 'Philippe Maygal',
'Dominique Pifarely', 'Dominique Pifarely',
'Ysé Tran' 'Ysé Tran'
], ],
directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'], directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'],
date: '1988' date: '1988'
}, },
{ {
start: '2023-02-07T16:05:00.000Z', start: '2023-02-07T16:05:00.000Z',
stop: '2023-02-07T17:45:00.000Z', stop: '2023-02-07T17:45:00.000Z',
title: 'Zilionáři', title: 'Zilionáři',
description: 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...', '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'], category: ['Drama', 'Akcia'],
actors: [ actors: [
'Zach Galifianakis', 'Zach Galifianakis',
'Kristen Wiigová', 'Kristen Wiigová',
'Owen Wilson', 'Owen Wilson',
'Kate McKinnon', 'Kate McKinnon',
'Leslie Jones', 'Leslie Jones',
'Jason Sudeikis', 'Jason Sudeikis',
'Ross Kimball', 'Ross Kimball',
'Devin Ratray', 'Devin Ratray',
'Mary Elizabeth Ellisová', 'Mary Elizabeth Ellisová',
'Jon Daly', 'Jon Daly',
'Ken Marino', 'Ken Marino',
'Daniel Zacapa', 'Daniel Zacapa',
'Tom Werme', 'Tom Werme',
'Njema Williams', 'Njema Williams',
'Nils Cruz', 'Nils Cruz',
'Michael Fraguada', 'Michael Fraguada',
'Christian Gonzalez', 'Christian Gonzalez',
'Candace Blanchard', 'Candace Blanchard',
'Karsten Friske', 'Karsten Friske',
'Dallas Edwards', 'Dallas Edwards',
'Barry Ratcliffe', 'Barry Ratcliffe',
'Shelton Grant', 'Shelton Grant',
'Laura Palka', 'Laura Palka',
'Reegus Flenory', 'Reegus Flenory',
'Wynn Reichert', 'Wynn Reichert',
'Jill Jane Clements', 'Jill Jane Clements',
'Joseph S. Wilson', 'Joseph S. Wilson',
'Jee An', 'Jee An',
'Rhoda Griffisová', 'Rhoda Griffisová',
'Nicole Dupre Sobchack' 'Nicole Dupre Sobchack'
], ],
directors: [ directors: [
'Scott August', 'Scott August',
'Richard L. Fox', 'Richard L. Fox',
'Michelle Malley-Campos', 'Michelle Malley-Campos',
'Sebastian Mazzola', 'Sebastian Mazzola',
'Steven Ritzi', 'Steven Ritzi',
'Pete Waterman', 'Pete Waterman',
'Jared Hess' 'Jared Hess'
], ],
date: '2016' date: '2016'
} }
]) ])
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
parser({ parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
channel, channel,
date date
}) })
.then(result => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(done) .catch(done)
}) })

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./hoy.tv.config.js') const { parser, url } = require('./hoy.tv.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-09-13', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '76', site_id: '76',
xmltv_id: 'HOYIBC.hk', xmltv_id: 'HOYIBC.hk',
lang: 'zh' lang: 'zh'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml') expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml'), 'utf8')
const result = parser({ content, channel, date }).map(p => { const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2024-09-13T03:30:00.000Z', start: '2024-09-13T03:30:00.000Z',
stop: '2024-09-13T04:30:00.000Z', stop: '2024-09-13T04:30:00.000Z',
title: '點講都係一家人[PG]', title: '點講都係一家人[PG]',
sub_title: '第46集' sub_title: '第46集'
}, },
{ {
start: '2024-09-13T04:30:00.000Z', start: '2024-09-13T04:30:00.000Z',
stop: '2024-09-13T05:30:00.000Z', stop: '2024-09-13T05:30:00.000Z',
title: '麝香之路', title: '麝香之路',
description: description:
'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world' '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 { parser, url } = require('./i24news.tv.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'ar', site_id: 'ar',
xmltv_id: 'I24NewsArabic.il' xmltv_id: 'I24NewsArabic.il'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules') expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => { const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-03-06T13:00:00.000Z', start: '2022-03-06T13:00:00.000Z',
stop: '2022-03-06T13:28:00.000Z', stop: '2022-03-06T13:28:00.000Z',
title: 'تغطية خاصة', title: 'تغطية خاصة',
description: 'Special Edition', description: 'Special Edition',
image: image:
'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png' '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', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '[]', content: '[]',
date date
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,56 +1,56 @@
const { parser, url } = require('./mewatch.sg.config.js') const { parser, url } = require('./mewatch.sg.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-06-11', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-06-11', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '97098', site_id: '97098',
xmltv_id: 'Channel5Singapore.sg' xmltv_id: 'Channel5Singapore.sg'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( 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' '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', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-06-11T21:00:00.000Z', start: '2022-06-11T21:00:00.000Z',
stop: '2022-06-11T21:30:00.000Z', stop: '2022-06-11T21:30:00.000Z',
title: 'Open Homes S3 - EP 2', title: 'Open Homes S3 - EP 2',
description: 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.', '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: 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", "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, episode: 2,
season: 3, season: 3,
rating: { rating: {
system: 'IMDA', system: 'IMDA',
value: 'G' value: 'G'
} }
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: content:
fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
channel channel
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

@@ -1,67 +1,67 @@
const { parser, url } = require('./mi.tv.config.js') const { parser, url } = require('./mi.tv.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'ar#24-7-canal-de-noticias', site_id: 'ar#24-7-canal-de-noticias',
xmltv_id: '247CanaldeNoticias.ar' xmltv_id: '247CanaldeNoticias.ar'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0' 'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => { const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-24T03:00:00.000Z', start: '2021-11-24T03:00:00.000Z',
stop: '2021-11-24T23:00:00.000Z', stop: '2021-11-24T23:00:00.000Z',
title: 'Trasnoche de 24/7', title: 'Trasnoche de 24/7',
category: 'Interés general', category: 'Interés general',
description: 'Lo más visto de la semana en nuestra pantalla.', description: 'Lo más visto de la semana en nuestra pantalla.',
image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
}, },
{ {
start: '2021-11-24T23:00:00.000Z', start: '2021-11-24T23:00:00.000Z',
stop: '2021-11-25T01:00:00.000Z', stop: '2021-11-25T01:00:00.000Z',
title: 'Noticiero central - Segunda edición', title: 'Noticiero central - Segunda edición',
category: 'Noticiero', category: 'Noticiero',
description: 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.', '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' image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
}, },
{ {
start: '2021-11-25T01:00:00.000Z', start: '2021-11-25T01:00:00.000Z',
stop: '2021-11-25T02:00:00.000Z', stop: '2021-11-25T02:00:00.000Z',
title: 'Plus energético', title: 'Plus energético',
category: 'Cultural', category: 'Cultural',
description: 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.', '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' image: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '' content: ''
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

@@ -1,192 +1,192 @@
const doFetch = require('@ntlab/sfetch') const doFetch = require('@ntlab/sfetch')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const crypto = require('crypto') const crypto = require('crypto')
const { sortBy } = require('../../scripts/functions') const { sortBy } = require('../../scripts/functions')
// API Configuration Constants // API Configuration Constants
const NATCO_CODE = 'hr' const NATCO_CODE = 'hr'
const APP_LANGUAGE = 'hr' const APP_LANGUAGE = 'hr'
const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5' const APP_KEY = 'GWaBW4RTloLwpUgYVzOiW5zUxFLmoMj5'
const APP_VERSION = '02.0.1080' const APP_VERSION = '02.0.1080'
const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE' const NATCO_KEY = 'l2lyvGVbUm2EKJE96ImQgcc8PKMZWtbE'
const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr' const SITE_URL = 'mojmaxtv.hrvatskitelekom.hr'
// Role Types // Role Types
const ROLE_TYPES = { const ROLE_TYPES = {
ACTOR: 'GLUMI', // Croatian for "ACTS" ACTOR: 'GLUMI', // Croatian for "ACTS"
DIRECTOR: 'REŽIJA', // Croatian for "DIRECTOR" DIRECTOR: 'REŽIJA', // Croatian for "DIRECTOR"
PRODUCER: 'PRODUKCIJA', // Croatian for "PRODUCER" PRODUCER: 'PRODUKCIJA', // Croatian for "PRODUCER"
WRITER: 'AUTOR', WRITER: 'AUTOR',
SCENARIO: 'SCENARIJ' SCENARIO: 'SCENARIJ'
} }
// Dynamic API Endpoint based on NATCO_CODE // Dynamic API Endpoint based on NATCO_CODE
const API_ENDPOINT = `https://tv-${NATCO_CODE}-prod.yo-digital.com/${NATCO_CODE}-bifrost` const API_ENDPOINT = `https://tv-${NATCO_CODE}-prod.yo-digital.com/${NATCO_CODE}-bifrost`
// Session/Device IDs // Session/Device IDs
const DEVICE_ID = crypto.randomUUID() const DEVICE_ID = crypto.randomUUID()
const SESSION_ID = crypto.randomUUID() const SESSION_ID = crypto.randomUUID()
const cached = {} const cached = {}
const getHeaders = () => ({ const getHeaders = () => ({
'app_key': APP_KEY, 'app_key': APP_KEY,
'app_version': APP_VERSION, 'app_version': APP_VERSION,
'device-id': DEVICE_ID, 'device-id': DEVICE_ID,
'tenant': 'tv', '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', '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}`, 'origin': `https://${SITE_URL}`,
'x-request-session-id': SESSION_ID, 'x-request-session-id': SESSION_ID,
'x-request-tracking-id': crypto.randomUUID(), 'x-request-tracking-id': crypto.randomUUID(),
'x-tv-step': 'EPG_SCHEDULES', 'x-tv-step': 'EPG_SCHEDULES',
'x-tv-flow': 'EPG', 'x-tv-flow': 'EPG',
'x-call-type': 'GUEST_USER', 'x-call-type': 'GUEST_USER',
'x-user-agent': `web|web|Chrome-133|${APP_VERSION}|1` 'x-user-agent': `web|web|Chrome-133|${APP_VERSION}|1`
}) })
module.exports = { module.exports = {
site: SITE_URL, site: SITE_URL,
url({ date }) { url({ date }) {
return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format( return `${API_ENDPOINT}/epg/channel/schedules?date=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}` )}&hour_offset=0&hour_range=3&channelMap_id&filler=true&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`
}, },
request: { request: {
headers: getHeaders(), headers: getHeaders(),
cache: { cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
} }
}, },
async parser({ content, channel, date }) { async parser({ content, channel, date }) {
const data = parseData(content) const data = parseData(content)
if (!data) return [] if (!data) return []
let items = parseItems(data, channel) let items = parseItems(data, channel)
if (!items.length) return [] if (!items.length) return []
const queue = [3, 6, 9, 12, 15, 18, 21] const queue = [3, 6, 9, 12, 15, 18, 21]
.map(offset => { .map(offset => {
const url = module.exports.url({ date }).replace('hour_offset=0', `hour_offset=${offset}`) const url = module.exports.url({ date }).replace('hour_offset=0', `hour_offset=${offset}`)
const params = { ...module.exports.request, headers: getHeaders() } const params = { ...module.exports.request, headers: getHeaders() }
if (cached[url]) { if (cached[url]) {
items = items.concat(parseItems(cached[url], channel)) items = items.concat(parseItems(cached[url], channel))
return null return null
} }
return { url, params } return { url, params }
}) })
.filter(Boolean) .filter(Boolean)
await doFetch(queue, (_req, _data) => { await doFetch(queue, (_req, _data) => {
if (_data) { if (_data) {
cached[_req.url] = _data cached[_req.url] = _data
items = items.concat(parseItems(_data, channel)) items = items.concat(parseItems(_data, channel))
} }
}) })
items = sortBy(items, i => dayjs(i.start_time).valueOf()) items = sortBy(items, i => dayjs(i.start_time).valueOf())
// Fetch program details for each item // Fetch program details for each item
const programs = [] const programs = []
for (let item of items) { for (let item of items) {
const detail = await loadProgramDetails(item) const detail = await loadProgramDetails(item)
// detectUnknownRoles(detail) // detectUnknownRoles(detail)
programs.push({ programs.push({
title: item.description, title: item.description,
sub_title: item.episode_name, sub_title: item.episode_name,
description: parseDescription(detail), description: parseDescription(detail),
categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [], categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : [],
date: parseDate(item), date: parseDate(item),
image: detail.poster_image_url, image: detail.poster_image_url,
actors: parseRoles(detail, ROLE_TYPES.ACTOR), actors: parseRoles(detail, ROLE_TYPES.ACTOR),
directors: parseRoles(detail, ROLE_TYPES.DIRECTOR), directors: parseRoles(detail, ROLE_TYPES.DIRECTOR),
producers: parseRoles(detail, ROLE_TYPES.PRODUCER), producers: parseRoles(detail, ROLE_TYPES.PRODUCER),
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
rating: parseRating(item), rating: parseRating(item),
start: item.start_time, start: item.start_time,
stop: item.end_time stop: item.end_time
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get( .get(
`${API_ENDPOINT}/epg/channel?channelMap_id=&includeVirtualChannels=false&natco_key=${NATCO_KEY}&app_language=${APP_LANGUAGE}&natco_code=${NATCO_CODE}`, `${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() } { ...module.exports.request, headers: getHeaders() }
) )
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
return data.channels.map(channel => ({ return data.channels.map(channel => ({
lang: NATCO_CODE, lang: NATCO_CODE,
name: channel.title, name: channel.title,
site_id: channel.station_id site_id: channel.station_id
})) }))
} }
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
if (!item.program_id) return {} if (!item.program_id) return {}
const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}` const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=${NATCO_CODE}`
const data = await axios const data = await axios
.get(url, { headers: getHeaders() }) .get(url, { headers: getHeaders() })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data || {} return data || {}
} }
function parseData(content) { function parseData(content) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
return data || null return data || null
} catch { } catch {
return null return null
} }
} }
function parseItems(data, channel) { function parseItems(data, channel) {
if (!data.channels || !Array.isArray(data.channels[channel.site_id])) return [] if (!data.channels || !Array.isArray(data.channels[channel.site_id])) return []
return data.channels[channel.site_id] return data.channels[channel.site_id]
} }
function parseDate(item) { function parseDate(item) {
return item && item.release_year ? item.release_year.toString() : null return item && item.release_year ? item.release_year.toString() : null
} }
function parseRating(item) { function parseRating(item) {
return item.ratings return item.ratings
? { ? {
system: 'MPA', system: 'MPA',
value: item.ratings value: item.ratings
} }
: null : null
} }
function parseSeason(item) { function parseSeason(item) {
if (item.season_display_number === 'Epizode') return null // 'Epizode' is 'Episodes' in Croatian if (item.season_display_number === 'Epizode') return null // 'Epizode' is 'Episodes' in Croatian
return item.season_number return item.season_number
} }
function parseEpisode(item) { function parseEpisode(item) {
if (item.episode_number) return parseInt(item.episode_number) if (item.episode_number) return parseInt(item.episode_number)
if (item.season_display_number === 'Epizode') return item.season_number if (item.season_display_number === 'Epizode') return item.season_number
return null return null
} }
function parseDescription(item) { function parseDescription(item) {
if (!item.details) return null if (!item.details) return null
return item.details.description return item.details.description
} }
function parseRoles(item, role_name) { function parseRoles(item, role_name) {
if (!item.roles) return null if (!item.roles) return null
return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) 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 doFetch = require('@ntlab/sfetch')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const { sortBy } = require('../../scripts/functions') const { sortBy } = require('../../scripts/functions')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'mtel.ba', site: 'mtel.ba',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
const [platform] = channel.site_id.split('#') 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( return `https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-${platform}&pageSize=999&date=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}` )}`
}, },
request: { request: {
timeout: 20000, // 20 seconds timeout: 20000, // 20 seconds
maxContentLength: 10000000, // 10 Mb maxContentLength: 10000000, // 10 Mb
cache: { cache: {
interpretHeader: false, interpretHeader: false,
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
} }
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description, description: item.description,
categories: parseCategories(item), categories: parseCategories(item),
image: parseImage(item), image: parseImage(item),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
}) })
return programs return programs
}, },
async channels({ platform = 'msat' }) { async channels({ platform = 'msat' }) {
const platforms = { const platforms = {
msat: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-msat', 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' iptv: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-iptv'
} }
const queue = [ const queue = [
{ {
platform, platform,
url: platforms[platform] url: platforms[platform]
} }
] ]
let channels = [] let channels = []
await doFetch(queue, (req, data) => { await doFetch(queue, (req, data) => {
if (data && data.pagination.currentPage < data.pagination.totalPages) { if (data && data.pagination.currentPage < data.pagination.totalPages) {
queue.push({ queue.push({
platform: req.platform, platform: req.platform,
url: platforms[req.platform] url: platforms[req.platform]
}) })
} }
data.products.forEach(channel => { data.products.forEach(channel => {
channels.push({ channels.push({
lang: 'bs', lang: 'bs',
name: channel.name, name: channel.name,
site_id: `${req.platform}#${channel.code}` site_id: `${req.platform}#${channel.code}`
}) })
}) })
}) })
return channels return channels
} }
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.start, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo') return dayjs.tz(item.start, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz(item.end, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo') return dayjs.tz(item.end, 'YYYY-MM-DD HH:mm', 'Europe/Sarajevo')
} }
function parseCategories(item) { function parseCategories(item) {
return item.category ? item.category.split(' / ') : [] return item.category ? item.category.split(' / ') : []
} }
function parseImage(item) { function parseImage(item) {
return item?.picture?.url ? item.picture.url : null return item?.picture?.url ? item.picture.url : null
} }
function parseItems(content, channel) { function parseItems(content, channel) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.products)) return [] if (!data || !Array.isArray(data.products)) return []
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const channelData = data.products.find(channel => channel.code === channelId) const channelData = data.products.find(channel => channel.code === channelId)
if (!channelData || !Array.isArray(channelData.programs)) return [] if (!channelData || !Array.isArray(channelData.programs)) return []
// filter out programs that have the sentence "no program information available" // filter out programs that have the sentence "no program information available"
channelData.programs = channelData.programs.filter(p => !p.title.includes('Nema informacija o programu')) channelData.programs = channelData.programs.filter(p => !p.title.includes('Nema informacija o programu'))
return sortBy(channelData.programs, p => parseStart(p).valueOf()) return sortBy(channelData.programs, p => parseStart(p).valueOf())
} catch { } catch {
return [] return []
} }
} }

View File

@@ -1,58 +1,58 @@
const { parser, url } = require('./mtel.ba.config.js') const { parser, url } = require('./mtel.ba.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'msat#ch-11-rtrs' } const channel = { site_id: 'msat#ch-11-rtrs' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat&pageSize=999&date=2025-02-04' 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat&pageSize=999&date=2025-02-04'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ channel, content }) let results = parser({ channel, content })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(38) expect(results.length).toBe(38)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-02-03T22:38:00.000Z', start: '2025-02-03T22:38:00.000Z',
stop: '2025-02-03T23:38:00.000Z', stop: '2025-02-03T23:38:00.000Z',
title: 'Neka pesma kaže', title: 'Neka pesma kaže',
image: image:
'https://medias.services.mtel.ba/medias/407368591.jpg?context=bWFzdGVyfHJvb3R8MTM2MTZ8aW1hZ2UvanBlZ3xhR1F5TDJnell5ODBOekExTmpFMk1qRTJNRFkzTUM4ME1EY3pOamcxT1RFdWFuQm58ZWM3Zjc4MDNlZTY5OWU1ZGJiZDI5N2UzMDg4ODA3NzQ1NWM0OThlMjdhYmU4MjI4NGJhOWE2YzYwMTc5ODM3NQ', 'https://medias.services.mtel.ba/medias/407368591.jpg?context=bWFzdGVyfHJvb3R8MTM2MTZ8aW1hZ2UvanBlZ3xhR1F5TDJnell5ODBOekExTmpFMk1qRTJNRFkzTUM4ME1EY3pOamcxT1RFdWFuQm58ZWM3Zjc4MDNlZTY5OWU1ZGJiZDI5N2UzMDg4ODA3NzQ1NWM0OThlMjdhYmU4MjI4NGJhOWE2YzYwMTc5ODM3NQ',
description: description:
'Zabavni-muzički program donosi nam divne zvukove prave, narodne muzike, u kojoj se izvođači oslanjaju na kvalitet i tradiciju.', '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'] categories: ['Music', 'Ballet', 'Dance']
}) })
expect(results[37]).toMatchObject({ expect(results[37]).toMatchObject({
start: '2025-02-04T22:27:00.000Z', start: '2025-02-04T22:27:00.000Z',
stop: '2025-02-04T23:58:00.000Z', stop: '2025-02-04T23:58:00.000Z',
title: 'Bitanga s plaže', title: 'Bitanga s plaže',
image: image:
'https://medias.services.mtel.ba/medias/117604203.jpg?context=bWFzdGVyfHJvb3R8MTY1MTZ8aW1hZ2UvanBlZ3xhRGd6TDJnek1DODBOekExTmpFMk16STNORGM0TWk4eE1UYzJNRFF5TURNdWFuQm58YmU5MjdkOTljMGE4YjIyNjg3ZmI1YWJjYWQ0ZDY5YjA0YWJiY2RlN2E0ZGVjOTdlYzM4MzI4MzYyMzFiODBlMg', 'https://medias.services.mtel.ba/medias/117604203.jpg?context=bWFzdGVyfHJvb3R8MTY1MTZ8aW1hZ2UvanBlZ3xhRGd6TDJnek1DODBOekExTmpFMk16STNORGM0TWk4eE1UYzJNRFF5TURNdWFuQm58YmU5MjdkOTljMGE4YjIyNjg3ZmI1YWJjYWQ0ZDY5YjA0YWJiY2RlN2E0ZGVjOTdlYzM4MzI4MzYyMzFiODBlMg',
description: 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.', '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'] categories: ['Movie', 'Drama']
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
channel, channel,
content: '{}' content: '{}'
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,107 +1,107 @@
const { parser, url, request } = require('./programme-tv.vini.pf.config.js') const { parser, url, request } = require('./programme-tv.vini.pf.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'tf1', site_id: 'tf1',
xmltv_id: 'TF1.fr' xmltv_id: 'TF1.fr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://programme-tv.vini.pf/programmesJSON') expect(url).toBe('https://programme-tv.vini.pf/programmesJSON')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' }) expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' })
}) })
it('can parse response', done => { it('can parse response', done => {
axios.post.mockImplementation((url, data) => { axios.post.mockImplementation((url, data) => {
if (data.dateDebut === '2021-11-20T16:00:00-10:00') { if (data.dateDebut === '2021-11-20T16:00:00-10:00') {
return Promise.resolve({ return Promise.resolve({
data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json')))
}) })
} else { } else {
return Promise.resolve({ return Promise.resolve({
data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json'))) data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_2.json')))
}) })
} }
}) })
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
parser({ content, channel, date }) parser({ content, channel, date })
.then(result => { .then(result => {
result = result.map(p => { result = result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-20T23:50:00.000Z', start: '2021-11-20T23:50:00.000Z',
stop: '2021-11-21T01:10:00.000Z', stop: '2021-11-21T01:10:00.000Z',
title: 'Reportages découverte', title: 'Reportages découverte',
category: 'Magazine', category: 'Magazine',
description: 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.", "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: image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg' 'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg'
}, },
{ {
start: '2021-11-21T01:10:00.000Z', start: '2021-11-21T01:10:00.000Z',
stop: '2021-11-21T02:30:00.000Z', stop: '2021-11-21T02:30:00.000Z',
title: 'Les docs du week-end', title: 'Les docs du week-end',
category: 'Magazine', category: 'Magazine',
description: 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 ?', '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: image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg' 'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg'
}, },
{ {
start: '2021-11-21T02:30:00.000Z', start: '2021-11-21T02:30:00.000Z',
stop: '2021-11-21T03:45:00.000Z', stop: '2021-11-21T03:45:00.000Z',
title: '50mn Inside', title: '50mn Inside',
category: 'Magazine', category: 'Magazine',
description: description:
"50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,", "50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,",
image: image:
'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg' 'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg'
} }
]) ])
done() done()
}) })
.catch(err => { .catch(err => {
done(err) done(err)
}) })
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
parser({ parser({
date, date,
channel, channel,
content: content:
'' ''
}) })
.then(result => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(err => { .catch(err => {
done(err) done(err)
}) })
}) })

View File

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

View File

@@ -1,50 +1,50 @@
const { parser, url } = require('./raiplay.it.config.js') const { parser, url } = require('./raiplay.it.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-05-03', 'YYYY-MM-DD') const date = dayjs.utc('2022-05-03', 'YYYY-MM-DD')
const channel = { const channel = {
site_id: 'rai-2', site_id: 'rai-2',
xmltv_id: 'Rai2.it' xmltv_id: 'Rai2.it'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json') expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => { const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-05-03T17:40:00.000Z', start: '2022-05-03T17:40:00.000Z',
stop: '2022-05-03T18:30:00.000Z', stop: '2022-05-03T18:30:00.000Z',
title: 'The Good Doctor S3E5 - La prima volta', title: 'The Good Doctor S3E5 - La prima volta',
description: 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.", "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', season: '3',
episode: '5', episode: '5',
sub_title: 'La prima volta', sub_title: 'La prima volta',
image: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg', 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' 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', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

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

View File

@@ -1,48 +1,48 @@
const { parser, url } = require('./rtmklik.rtm.gov.my.config.js') const { parser, url } = require('./rtmklik.rtm.gov.my.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-09-04', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-09-04', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '2', site_id: '2',
xmltv_id: 'TV2.my' xmltv_id: 'TV2.my'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0' 'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date }).map(p => { const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-09-04T19:00:00.000Z', start: '2022-09-04T19:00:00.000Z',
stop: '2022-09-04T20:00:00.000Z', stop: '2022-09-04T20:00:00.000Z',
title: 'Hope Of Life', title: 'Hope Of Life',
description: 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.' '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', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

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

View File

@@ -1,41 +1,41 @@
const { url, parser } = require('./shahid.mbc.net.config.js') const { url, parser } = require('./shahid.mbc.net.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-11-11').startOf('d') const date = dayjs.utc('2023-11-11').startOf('d')
const channel = { site_id: '996520', xmltv_id: 'AlAanTV.ae', lang: 'en' } const channel = { site_id: '996520', xmltv_id: 'AlAanTV.ae', lang: 'en' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
`https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${ `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${
channel.site_id channel.site_id
}&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format( }&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}` )}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}`
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel, date }) const result = parser({ content, channel, date })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2023-11-10T21:00:00.000Z', start: '2023-11-10T21:00:00.000Z',
stop: '2023-11-10T21:30:00.000Z', stop: '2023-11-10T21:30:00.000Z',
title: "Menassaatona Fi Osboo'", title: "Menassaatona Fi Osboo'",
description: 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." "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', () => { it('can handle empty guide', () => {
const result = parser({ content: '' }) const result = parser({ content: '' })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,54 +1,54 @@
const { parser, url, request } = require('./siba.com.co.config.js') const { parser, url, request } = require('./siba.com.co.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '395', site_id: '395',
xmltv_id: 'CanalClaro.cl' xmltv_id: 'CanalClaro.cl'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla') expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const result = request.data({ channel, date }) const result = request.data({ channel, date })
expect(result.has('servicio')).toBe(true) expect(result.has('servicio')).toBe(true)
expect(result.has('ini')).toBe(true) expect(result.has('ini')).toBe(true)
expect(result.has('end')).toBe(true) expect(result.has('end')).toBe(true)
expect(result.has('chn')).toBe(true) expect(result.has('chn')).toBe(true)
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content }) const result = parser({ date, channel, content })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-11T00:00:00.000Z', start: '2021-11-11T00:00:00.000Z',
stop: '2021-11-11T01:00:00.000Z', stop: '2021-11-11T01:00:00.000Z',
title: 'Worst Cooks In America' title: 'Worst Cooks In America'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

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

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./stod2.is.config.js') const { parser, url } = require('./stod2.is.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(timezone) dayjs.extend(timezone)
const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day')
const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' } const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' }
it('can generate valid url', () => { it('can generate valid url', () => {
const generatedUrl = url({ date, channel }) const generatedUrl = url({ date, channel })
expect(generatedUrl).toBe('https://api.stod2.is/dagskra/api/stod2/2025-01-03') expect(generatedUrl).toBe('https://api.stod2.is/dagskra/api/stod2/2025-01-03')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => { const result = parser({ content }).map(p => {
p.start = p.start.toISOString() p.start = p.start.toISOString()
p.stop = p.stop.toISOString() p.stop = p.stop.toISOString()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: 'Heimsókn', title: 'Heimsókn',
sub_title: 'Telma Borgþórsdóttir', sub_title: 'Telma Borgþórsdóttir',
description: 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.', '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: '', actors: '',
directors: '', directors: '',
start: '2025-01-03T08:00:00.000Z', start: '2025-01-03T08:00:00.000Z',
stop: '2025-01-03T08:15:00.000Z' stop: '2025-01-03T08:15:00.000Z'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ content: '[]' }) const result = parser({ content: '[]' })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

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

View File

@@ -1,49 +1,49 @@
const { parser, url } = require('./tapdmv.com.config.js') const { parser, url } = require('./tapdmv.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756', site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756',
xmltv_id: 'TAPActionFlix.ph' xmltv_id: 'TAPActionFlix.ph'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( 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' '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', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, date }).map(p => { const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-10-04T01:00:00.000Z', start: '2022-10-04T01:00:00.000Z',
stop: '2022-10-04T02:25:00.000Z', stop: '2022-10-04T02:25:00.000Z',
title: 'The Devil Inside', title: 'The Devil Inside',
description: 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.', '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', category: 'Horror',
image: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png' image: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')),
date date
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

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

View File

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

View File

@@ -1,60 +1,60 @@
const { parser, url } = require('./teliatv.ee.config.js') const { parser, url } = require('./teliatv.ee.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-20', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-20', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
lang: 'et', lang: 'et',
site_id: 'et#1', site_id: 'et#1',
xmltv_id: 'ETV.ee' xmltv_id: 'ETV.ee'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( 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' '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', () => { it('can generate valid url with different language', () => {
const ruChannel = { const ruChannel = {
lang: 'ru', lang: 'ru',
site_id: 'ru#1', site_id: 'ru#1',
xmltv_id: 'ETV.ee' xmltv_id: 'ETV.ee'
} }
expect(url({ date, channel: ruChannel })).toBe( 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' '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', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-19T22:05:00.000Z', start: '2021-11-19T22:05:00.000Z',
stop: '2021-11-19T22:55:00.000Z', stop: '2021-11-19T22:55:00.000Z',
title: 'Inimjaht', title: 'Inimjaht',
image: image:
'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg' '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', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,72 +1,72 @@
const { parser, url } = require('./tv.blue.ch.config.js') const { parser, url } = require('./tv.blue.ch.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '1221', site_id: '1221',
xmltv_id: 'BlueZoomD.ch' xmltv_id: 'BlueZoomD.ch'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)' 'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => { const result = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-01-16T23:30:00.000Z', start: '2022-01-16T23:30:00.000Z',
stop: '2022-01-17T00:00:00.000Z', stop: '2022-01-17T00:00:00.000Z',
title: 'Weekend on the Rocks', title: 'Weekend on the Rocks',
description: 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.', ' - «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: image:
'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp' 'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp'
} }
]) ])
}) })
it('can parse response without image', () => { it('can parse response without image', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_without_image.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_without_image.json'))
const result = parser({ content }).map(p => { const result = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-01-17T04:59:00.000Z', start: '2022-01-17T04:59:00.000Z',
stop: '2022-01-17T05:00:00.000Z', stop: '2022-01-17T05:00:00.000Z',
title: 'Lorem ipsum' title: 'Lorem ipsum'
} }
]) ])
}) })
it('can handle wrong site id', () => { it('can handle wrong site id', () => {
const result = parser({ const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/content_invalid_siteid.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/content_invalid_siteid.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View File

@@ -1,103 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels> <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="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="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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="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="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="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> <channel site="tv.dir.bg" lang="bg" xmltv_id="VivacomArena.bg" site_id="44">Vivacom Arena (Виваком Арена)</channel>
</channels> </channels>

View File

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

View File

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

View File

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

View File

@@ -1,147 +1,147 @@
const axios = require('axios') const axios = require('axios')
const crypto = require('crypto') const crypto = require('crypto')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost' const API_ENDPOINT = 'https://tv-at-prod.yo-digital.com/at-bifrost'
const headers = { const headers = {
'Device-Id': crypto.randomUUID(), 'Device-Id': crypto.randomUUID(),
app_key: 'CTnKA63ruKM0JM1doxAXwwyQLLmQiEiy', app_key: 'CTnKA63ruKM0JM1doxAXwwyQLLmQiEiy',
app_version: '02.0.1260', app_version: '02.0.1260',
'X-User-Agent': 'web|web|Firefox-120|02.0.1260|1', 'X-User-Agent': 'web|web|Firefox-120|02.0.1260|1',
'x-request-tracking-id': crypto.randomUUID() 'x-request-tracking-id': crypto.randomUUID()
} }
module.exports = { module.exports = {
site: 'tv.magenta.at', site: 'tv.magenta.at',
days: 2, days: 2,
request: { request: {
headers, headers,
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${ return `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${
channel.site_id channel.site_id
}&date=${date.format('YYYY-MM-DD')}&hour_offset=${date.format('H')}&hour_range=3&natco_code=at` }&date=${date.format('YYYY-MM-DD')}&hour_offset=${date.format('H')}&hour_range=3&natco_code=at`
}, },
async parser({ content, channel, date }) { async parser({ content, channel, date }) {
let programs = [] let programs = []
if (!content) return programs if (!content) return programs
let items = parseItems(JSON.parse(content), channel) let items = parseItems(JSON.parse(content), channel)
if (!items.length) return programs if (!items.length) return programs
const promises = [3, 6, 9, 12, 15, 18, 21].map(i => const promises = [3, 6, 9, 12, 15, 18, 21].map(i =>
axios.get( axios.get(
`${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${channel.site_id}&date=${date.format( `${API_ENDPOINT}/epg/channel/schedules/v2?station_ids=${channel.site_id}&date=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}&hour_offset=${i}&hour_range=3&natco_code=at`, )}&hour_offset=${i}&hour_range=3&natco_code=at`,
{ headers } { headers }
) )
) )
await Promise.allSettled(promises) await Promise.allSettled(promises)
.then(results => { .then(results => {
results.forEach(r => { results.forEach(r => {
if (r.status === 'fulfilled') { if (r.status === 'fulfilled') {
const parsed = parseItems(r.value.data, channel) const parsed = parseItems(r.value.data, channel)
items = items.concat(parsed) items = items.concat(parsed)
} }
}) })
}) })
.catch(console.error) .catch(console.error)
for (let item of items) { for (let item of items) {
const detail = await loadProgramDetails(item) const detail = await loadProgramDetails(item)
programs.push({ programs.push({
title: item.description, title: item.description,
description: parseDescription(detail), description: parseDescription(detail),
date: parseDate(item), date: parseDate(item),
category: parseCategory(item), category: parseCategory(item),
image: detail.poster_image_url, image: detail.poster_image_url,
actors: parseRoles(detail, 'Schauspieler'), actors: parseRoles(detail, 'Schauspieler'),
directors: parseRoles(detail, 'Regisseur'), directors: parseRoles(detail, 'Regisseur'),
producers: parseRoles(detail, 'Produzent'), producers: parseRoles(detail, 'Produzent'),
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get(`${API_ENDPOINT}/epg/channel?natco_code=at`, { headers }) .get(`${API_ENDPOINT}/epg/channel?natco_code=at`, { headers })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
return { return {
lang: 'de', lang: 'de',
site_id: item.station_id, site_id: item.station_id,
name: item.title name: item.title
} }
}) })
} }
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
if (!item.program_id) return {} if (!item.program_id) return {}
const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=at` const url = `${API_ENDPOINT}/details/series/${item.program_id}?natco_code=at`
const data = await axios const data = await axios
.get(url, { headers }) .get(url, { headers })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data || {} return data || {}
} }
function parseDate(item) { function parseDate(item) {
return item && item.release_year ? item.release_year.toString() : null return item && item.release_year ? item.release_year.toString() : null
} }
function parseStart(item) { function parseStart(item) {
return dayjs(item.start_time) return dayjs(item.start_time)
} }
function parseStop(item) { function parseStop(item) {
return dayjs(item.end_time) return dayjs(item.end_time)
} }
function parseItems(data, channel) { function parseItems(data, channel) {
if (!data || !data.channels) return [] if (!data || !data.channels) return []
const channelData = data.channels[channel.site_id] const channelData = data.channels[channel.site_id]
if (!channelData) return [] if (!channelData) return []
return channelData return channelData
} }
function parseCategory(item) { function parseCategory(item) {
if (!item.genres) return null if (!item.genres) return null
return item.genres.map(genre => genre.id) return item.genres.map(genre => genre.id)
} }
function parseSeason(item) { function parseSeason(item) {
if (item.season_display_number === 'Folgen') return null if (item.season_display_number === 'Folgen') return null
return item.season_number return item.season_number
} }
function parseEpisode(item) { function parseEpisode(item) {
if (item.episode_number) return parseInt(item.episode_number) if (item.episode_number) return parseInt(item.episode_number)
if (item.season_display_number === 'Folgen') return item.season_number if (item.season_display_number === 'Folgen') return item.season_number
return null return null
} }
function parseDescription(item) { function parseDescription(item) {
if (!item.details) return null if (!item.details) return null
return item.details.description return item.details.description
} }
function parseRoles(item, role_name) { function parseRoles(item, role_name) {
if (!item.roles) return null if (!item.roles) return null
return item.roles.filter(role => role.role_name === role_name).map(role => role.person_name) 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 { DateTime } = require('luxon')
const axios = require('axios') const axios = require('axios')
const { uniqBy } = require('../../scripts/functions') const { uniqBy } = require('../../scripts/functions')
module.exports = { module.exports = {
site: 'tv.mail.ru', site: 'tv.mail.ru',
days: 2, days: 2,
delay: 1000, delay: 1000,
url({ channel, date }) { url({ channel, date }) {
return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${ return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${
channel.site_id channel.site_id
}&date=${date.format('YYYY-MM-DD')}` }&date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart(item, date) let start = parseStart(item, date)
if (prev) { if (prev) {
if (start < prev.start) { if (start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.plus({ hours: 1 }) const stop = start.plus({ hours: 1 })
programs.push({ programs.push({
title: item.name, title: item.name,
category: parseCategory(item), category: parseCategory(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const regions = [5506, 1096, 1125, 285] const regions = [5506, 1096, 1125, 285]
let channels = [] let channels = []
for (let region of regions) { for (let region of regions) {
const totalPages = await getTotalPageCount(region) const totalPages = await getTotalPageCount(region)
const pages = Array.from(Array(totalPages).keys()) const pages = Array.from(Array(totalPages).keys())
for (let page of pages) { for (let page of pages) {
const data = await axios const data = await axios
.get('https://tv.mail.ru/ajax/channel/list/', { .get('https://tv.mail.ru/ajax/channel/list/', {
params: { page }, params: { page },
headers: { headers: {
cookie: `s=fver=0|geo=${region};` cookie: `s=fver=0|geo=${region};`
} }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
data.channels.forEach(item => { data.channels.forEach(item => {
channels.push({ channels.push({
lang: 'ru', lang: 'ru',
name: item.name, name: item.name,
site_id: item.id site_id: item.id
}) })
}) })
} }
} }
return uniqBy(channels, 'site_id') return uniqBy(channels, 'site_id')
} }
} }
async function getTotalPageCount(region) { async function getTotalPageCount(region) {
const data = await axios const data = await axios
.get('https://tv.mail.ru/ajax/channel/list/', { .get('https://tv.mail.ru/ajax/channel/list/', {
params: { page: 0 }, params: { page: 0 },
headers: { headers: {
cookie: `s=fver=0|geo=${region};` cookie: `s=fver=0|geo=${region};`
} }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.total return data.total
} }
function parseStart(item, date) { function parseStart(item, date) {
const dateString = `${date.format('YYYY-MM-DD')} ${item.start}` const dateString = `${date.format('YYYY-MM-DD')} ${item.start}`
return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC() return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC()
} }
function parseCategory(item) { function parseCategory(item) {
const categories = { const categories = {
1: 'Фильм', 1: 'Фильм',
2: 'Сериал', 2: 'Сериал',
6: 'Документальное', 6: 'Документальное',
7: 'Телемагазин', 7: 'Телемагазин',
8: 'Позновательное', 8: 'Позновательное',
10: 'Другое', 10: 'Другое',
14: 'ТВ-шоу', 14: 'ТВ-шоу',
16: 'Досуг,Хобби', 16: 'Досуг,Хобби',
17: 'Ток-шоу', 17: 'Ток-шоу',
18: 'Юмористическое', 18: 'Юмористическое',
23: 'Музыка', 23: 'Музыка',
24: 'Развлекательное', 24: 'Развлекательное',
25: 'Игровое', 25: 'Игровое',
26: 'Новости' 26: 'Новости'
} }
return categories[item.category_id] return categories[item.category_id]
? { ? {
lang: 'ru', lang: 'ru',
value: categories[item.category_id] value: categories[item.category_id]
} }
: null : null
} }
function parseItems(content) { function parseItems(content) {
const json = JSON.parse(content) const json = JSON.parse(content)
if (!Array.isArray(json.schedule) || !json.schedule[0]) return [] if (!Array.isArray(json.schedule) || !json.schedule[0]) return []
const event = json.schedule[0].event || [] const event = json.schedule[0].event || []
return [...event.past, ...event.current] return [...event.past, ...event.current]
} }

View File

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

View File

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

View File

@@ -1,338 +1,338 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels> <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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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@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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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@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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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.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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="" 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="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="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="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="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="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="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="" 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="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="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="" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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> <channel site="tv2go.t-2.net" lang="sl" xmltv_id="ZdravaTV.si" site_id="1000616">Zdrava TV</channel>
</channels> </channels>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,97 +1,97 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
const { uniqBy } = require('../../scripts/functions') const { uniqBy } = require('../../scripts/functions')
module.exports = { module.exports = {
site: 'tvhebdo.com', site: 'tvhebdo.com',
days: 2, days: 2,
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format( return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}` )}`
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start < prev.start) { if (start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
} }
prev.stop = start prev.stop = start
} }
let stop = start.plus({ minutes: 30 }) let stop = start.plus({ minutes: 30 })
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
let items = [] let items = []
const offsets = [ const offsets = [
0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360
] ]
for (let offset of offsets) { 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` const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00`
console.log(url) console.log(url)
const html = await axios const html = await axios
.get(url, { .get(url, {
headers: { headers: {
Cookie: 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' '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) .then(r => r.data)
.catch(console.error) .catch(console.error)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const rows = $('table.gr_row').toArray() const rows = $('table.gr_row').toArray()
items = items.concat(rows) items = items.concat(rows)
} }
let channels = [] let channels = []
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr( const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr(
'alt' 'alt'
) )
const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href') const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href')
const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null] const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null]
channels.push({ channels.push({
lang: 'fr', lang: 'fr',
site_id, site_id,
name name
}) })
}) })
return uniqBy(channels, x => x.site_id) return uniqBy(channels, x => x.site_id)
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.titre').first().text().trim() return $item('.titre').first().text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('.heure').text() const time = $item('.heure').text()
return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'America/Toronto' zone: 'America/Toronto'
}).toUTC() }).toUTC()
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $( return $(
'#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]' '#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]'
).toArray() ).toArray()
} }

View File

@@ -1,45 +1,45 @@
const { parser, url } = require('./tvheute.at.config.js') const { parser, url } = require('./tvheute.at.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const path = require('path') const path = require('path')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const { readFileSync } = require('fs') const { readFileSync } = require('fs')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' } const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021' 'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
expect(parser({ date, channel, content: readFileSync(path.resolve(__dirname, './__data__/content.html'), 'utf8') })).toMatchObject([ expect(parser({ date, channel, content: readFileSync(path.resolve(__dirname, './__data__/content.html'), 'utf8') })).toMatchObject([
{ {
start: '2021-11-08T05:00:00.000Z', start: '2021-11-08T05:00:00.000Z',
stop: '2021-11-08T05:10:00.000Z', stop: '2021-11-08T05:10:00.000Z',
title: 'Monchhichi (Wh.)', title: 'Monchhichi (Wh.)',
category: 'Kids', category: 'Kids',
description: 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.', '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' image: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg'
}, },
{ {
start: '2021-11-08T17:00:00.000Z', start: '2021-11-08T17:00:00.000Z',
stop: '2021-11-08T17:10:00.000Z', stop: '2021-11-08T17:10:00.000Z',
title: 'ZIB 18' title: 'ZIB 18'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8') content: readFileSync(path.resolve(__dirname, './__data__/no_content.html'), 'utf8')
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })