epg-grabber
Version:
Node.js CLI tool for grabbing EPG from different sites
486 lines (450 loc) • 13.8 kB
text/typescript
/**
* @jest-environment node
*/
import { it, expect, beforeAll, afterAll, afterEach, beforeEach, describe, vi, test } from 'vitest'
import { EPGGrabber, EPGGrabberMock } from '../src/index'
import { SiteConfig } from '../src/types/siteConfig'
import * as epgGrabber from '../src/index'
import { http, HttpResponse } from 'msw'
import { pathToFileURL } from 'node:url'
import { setupServer } from 'msw/node'
import path from 'node:path'
import dayjs from 'dayjs'
import fs from 'fs-extra'
describe('EPGGrabber', () => {
describe('grab()', () => {
const restHandlers = [
http.get('http://example.com/20210319/1tv.json', () => {
return HttpResponse.json([{ title: 'Program1', start: '2021-03-19T04:30:00.000Z' }])
})
]
const server = setupServer(...restHandlers)
beforeAll(async () => {
server.listen({ onUnhandledRequest: 'error' })
})
beforeEach(() => vi.useFakeTimers())
afterAll(() => server.close())
afterEach(() => server.resetHandlers())
it('can use global config', async () => {
const config: SiteConfig = {
site: 'example.com',
url: 'http://example.com/20210319/1tv.json',
parser: ({ content }) => (content ? JSON.parse(content) : [])
}
const channel = new epgGrabber.Channel({
site: 'example.com',
site_id: '1',
xmltv_id: '1TV.fr',
lang: 'fr',
name: '1TV',
logo: null,
url: null,
lcn: null,
index: -1
})
const grabber = new EPGGrabber(config)
const promise = grabber.grab(channel, '2022-01-01', (context, error) => {
if (error) throw error
})
vi.advanceTimersByTime(3000)
const programs = await promise
expect(programs.length).toBe(1)
expect(programs[0].toObject()).toMatchObject({
site: 'example.com',
channel: '1TV.fr',
titles: [{ value: 'Program1', lang: 'fr' }],
subTitles: [],
descriptions: [],
icons: [],
episodeNumbers: [],
date: 0,
start: 1616128200000,
stop: 0,
urls: [],
ratings: [],
categories: [],
directors: [],
actors: [],
writers: [],
adapters: [],
audio: {},
video: {},
images: [],
keywords: [],
languages: [],
lastChance: [],
length: [],
new: false,
origLanguages: [],
premiere: [],
previouslyShown: [],
reviews: [],
starRatings: [],
subtitles: [],
countries: [],
producers: [],
composers: [],
editors: [],
presenters: [],
commentators: [],
guests: []
})
})
it('can use local configs', async () => {
const config: SiteConfig = {
site: 'example.com',
url: 'http://example.com/20210319/1tv.json',
parser: ({ content }) => (content ? JSON.parse(content) : [])
}
const channel = new epgGrabber.Channel({
site: 'example.com',
site_id: '1',
xmltv_id: '1TV.fr',
lang: 'fr',
name: '1TV',
logo: null,
url: null,
lcn: null,
index: -1
})
const grabber = new EPGGrabber()
const promise = grabber.grab(channel, '2022-01-01', config, (context, error) => {
if (error) throw error
})
vi.advanceTimersByTime(3000)
const programs = await promise
expect(programs[0].titles).toMatchObject([
{
lang: 'fr',
value: 'Program1'
}
])
})
})
describe('loadLogo()', () => {
it('can load logo for channel', async () => {
const config: SiteConfig = {
site: 'example.com',
url: 'http://example.com/20210319/1tv.json',
parser: ({ content }) => (content ? JSON.parse(content) : []),
logo: ({ channel }) => `https://example.com/logos/${channel.xmltv_id}`
}
const channel = new epgGrabber.Channel({
site: 'example.com',
site_id: '1',
xmltv_id: '1TV.fr',
lang: 'fr',
name: '1TV',
logo: null,
url: null,
lcn: null,
index: -1
})
const grabber = new EPGGrabber()
const logo = await grabber.loadLogo(channel, '2022-01-01', config)
expect(logo).toBe('https://example.com/logos/1TV.fr')
})
})
describe('parseChannelsXML()', () => {
it('can parse channels.xml', () => {
const xml = fs.readFileSync(
path.resolve(__dirname, './__data__/input/example.channels.xml'),
'utf-8'
)
const channels = EPGGrabber.parseChannelsXML(xml)
expect(channels.length).toBe(2)
expect(channels[0]).toBeInstanceOf(epgGrabber.Channel)
expect(channels[1]).toBeInstanceOf(epgGrabber.Channel)
expect(channels[0].toObject()).toMatchObject({
site: 'example.com',
site_id: '1',
xmltv_id: '1TV.com',
lang: 'fr',
logo: 'https://example.com/logos/1TV.png',
name: '1 TV',
index: 0,
lcn: '36'
})
expect(channels[1].toObject()).toMatchObject({
site: 'example.com',
site_id: '2',
lang: null,
logo: null,
xmltv_id: '2TV.com',
name: '2 TV',
index: 1
})
})
it('can parse channels.xml with inline site attribute', () => {
const xml = fs.readFileSync(
path.resolve(__dirname, './__data__/input/example_3.channels.xml'),
'utf-8'
)
const channels = EPGGrabber.parseChannelsXML(xml)
expect(channels.length).toBe(2)
expect(channels[0]).toBeInstanceOf(epgGrabber.Channel)
expect(channels[1]).toBeInstanceOf(epgGrabber.Channel)
expect(channels[0].toObject()).toMatchObject({
site: 'example.com',
site_id: '1',
xmltv_id: '1TV.com',
lang: 'fr',
logo: 'https://example.com/logos/1TV.png',
name: '1 TV'
})
expect(channels[1].toObject()).toMatchObject({
site: 'example.com',
site_id: '2',
lang: null,
logo: null,
xmltv_id: '2TV.com',
name: '2 TV'
})
})
it('can parse legacy channels.xml', () => {
const xml = fs.readFileSync(
path.resolve(__dirname, './__data__/input/legacy.channels.xml'),
'utf-8'
)
const channels = EPGGrabber.parseChannelsXML(xml)
expect(channels.length).toBe(2)
expect(channels[0]).toBeInstanceOf(epgGrabber.Channel)
expect(channels[1]).toBeInstanceOf(epgGrabber.Channel)
expect(channels[0].toObject()).toMatchObject({
site: 'example.com',
site_id: '1',
xmltv_id: '1TV.com',
lang: 'fr',
logo: 'https://example.com/logos/1TV.png',
name: '1 TV'
})
expect(channels[1].toObject()).toMatchObject({
site: 'example.com',
site_id: '2',
lang: null,
logo: null,
xmltv_id: '2TV.com',
name: '2 TV'
})
})
})
describe('generateXMLTV()', () => {
vi.useFakeTimers().setSystemTime(new Date('2022-05-05'))
const channels = [
new epgGrabber.Channel({
xmltv_id: '1TV.co',
name: '1 TV',
site: 'example.com',
logo: 'https://example.com/channel_one_icon.jpg',
index: 1,
url: 'https://example.com/channel_one?foo=foo&bar=bar',
lcn: '36',
site_id: '#',
lang: null
}),
new epgGrabber.Channel({
xmltv_id: '2TV.co',
name: '2 TV',
site: 'example.com',
site_id: '#',
url: null,
lcn: null,
index: 2,
lang: 'es',
logo: 'https://example.com/logos/2TV.png'
}),
new epgGrabber.Channel({
xmltv_id: '3TV.co',
name: '3 TV',
site: 'example.com',
site_id: '#',
url: null,
lcn: null,
lang: null,
logo: null,
index: 3
})
]
it('can generate xmltv', () => {
const programs = [
new epgGrabber.Program({
site: 'example.com',
channel: '1TV.co',
start: 1616133600000,
stop: 1616135400000,
titles: [{ value: 'Program 1' }],
subTitles: [{ value: 'Sub-title & 1' }],
descriptions: [{ value: 'Description for Program 1' }],
date: 1651795200000,
categories: [{ value: 'Test' }],
keywords: [
{ lang: 'en', value: 'physical-comedy' },
{ lang: 'en', value: 'romantic' }
],
languages: [{ value: 'English' }],
origLanguages: [{ lang: 'en', value: 'French' }],
length: [{ units: 'minutes', value: '60' }],
urls: [{ value: 'http://example.com/title.html' }],
countries: [{ value: 'US' }],
video: {
present: 'yes',
colour: 'no',
aspect: '16:9',
quality: 'HDTV'
},
audio: {
present: 'yes',
stereo: 'Dolby Digital'
},
episodeNumbers: [
{ system: 'xmltv_ns', value: '8.238.0/1' },
{ system: 'onscreen', value: 'S09E239' }
],
previouslyShown: [{ start: '', channel: '' }],
premiere: [{ value: 'First time on British TV' }],
lastChance: [{ lang: 'en', value: 'Last time on this channel' }],
new: true,
subtitles: [
{ type: 'teletext', language: [{ value: 'English' }] },
{ type: 'onscreen', language: [{ lang: 'en', value: 'Spanish' }] }
],
ratings: [
{
system: 'MPAA',
value: 'P&G',
icons: [{ src: 'http://example.com/pg_symbol.png' }]
}
],
starRatings: [
{
system: 'TV Guide',
value: '4/5',
icons: [{ src: 'stars.png' }]
},
{
system: 'IMDB',
value: '8/10',
icons: []
}
],
reviews: [
{
type: 'text',
source: 'Rotten Tomatoes',
reviewer: 'Joe Bloggs',
lang: 'en',
value: 'This is a fantastic show!'
},
{
type: 'text',
source: 'IDMB',
reviewer: 'Jane Doe',
lang: 'en',
value: 'I love this show!'
},
{
type: 'url',
source: 'Rotten Tomatoes',
reviewer: 'Joe Bloggs',
lang: 'en',
value: 'https://example.com/programme_one_review'
}
],
directors: [
{
value: 'Director 1',
urls: [{ value: 'http://example.com/director1.html', system: 'TestSystem' }],
images: [
{ value: 'https://example.com/image1.jpg' },
{
value: 'https://example.com/image2.jpg',
type: 'person',
size: '2',
system: 'TestSystem',
orient: 'P'
}
]
},
{
value: 'Director 2',
urls: [],
images: []
}
],
actors: [
{ value: 'Actor 1', urls: [], images: [] },
{ value: 'Actor 2', urls: [], images: [] }
],
writers: [{ value: 'Writer 1', urls: [], images: [] }],
images: [
{
type: 'poster',
size: '1',
orient: 'P',
system: 'tvdb',
value: 'https://tvdb.com/programme_one_poster_1.jpg?foo=foo&bar=bar'
},
{
type: 'poster',
size: '2',
orient: 'P',
system: 'tmdb',
value: 'https://tmdb.com/programme_one_poster_2.jpg'
},
{
type: 'backdrop',
size: '3',
orient: 'L',
system: 'tvdb',
value: 'https://tvdb.com/programme_one_backdrop_3.jpg'
},
{
type: 'backdrop',
size: '3',
orient: 'L',
system: 'tmdb',
value: 'https://tmdb.com/programme_one_backdrop_3.jpg'
}
],
icons: [{ src: 'https://example.com/images/Program1.png?x=шеллы&sid=777' }]
}),
new epgGrabber.Program({
site: 'example.com',
channel: '2TV.co',
titles: [{ lang: 'es', value: 'Program 2' }],
start: 1616133600000,
stop: 1616135400000
})
]
const output = EPGGrabber.generateXMLTV(channels, programs, { date: dayjs.utc().format('YYYYMMDD') })
expect(output).toEqual(
fs.readFileSync(pathToFileURL('tests/__data__/expected/index.guide.xml'), 'utf8')
)
})
})
})
describe('EPGGrabberMock', () => {
test('grab()', async () => {
const config: SiteConfig = {
site: 'example.com',
url: 'http://example.com/20210319/1tv.json',
parser: ({ content }) => (content ? JSON.parse(content) : [])
}
const channel = new epgGrabber.Channel({
site: 'example.com',
site_id: '1',
xmltv_id: '1TV.fr',
lang: 'fr',
name: '1TV',
logo: null,
url: null,
lcn: null,
index: -1
})
const grabber = new EPGGrabberMock(config)
const programs = await grabber.grab(channel, '2022-01-01', (context, error) => {
if (error) throw error
})
expect(programs.length).toBe(0)
})
})