Fixes linter errors

This commit is contained in:
freearhey 2023-10-15 14:08:23 +03:00
parent 57e508fc3b
commit 63c86a2b30
393 changed files with 28447 additions and 28443 deletions

View file

@ -1,18 +1,18 @@
import { Logger } from '@freearhey/core'
import { ApiClient } from '../../core'
async function main() {
const logger = new Logger()
const client = new ApiClient({ logger })
const requests = [
client.download('channels.json'),
client.download('countries.json'),
client.download('regions.json'),
client.download('subdivisions.json')
]
await Promise.all(requests)
}
main()
import { Logger } from '@freearhey/core'
import { ApiClient } from '../../core'
async function main() {
const logger = new Logger()
const client = new ApiClient({ logger })
const requests = [
client.download('channels.json'),
client.download('countries.json'),
client.download('regions.json'),
client.download('subdivisions.json')
]
await Promise.all(requests)
}
main()

View file

@ -1,180 +1,180 @@
import { DATA_DIR } from '../../constants'
import { Storage, Collection, Dictionary, Logger } from '@freearhey/core'
import { ChannelsParser, XML, ApiChannel } from '../../core'
import { Channel } from 'epg-grabber'
import { transliterate } from 'transliteration'
import nodeCleanup from 'node-cleanup'
import { program } from 'commander'
import inquirer, { QuestionCollection } from 'inquirer'
program
.argument('<filepath>', 'Path to *.channels.xml file to edit')
.option('-c, --country <name>', 'Default country (ISO 3166 code)', 'US')
.parse(process.argv)
const filepath = program.args[0]
const programOptions = program.opts()
const defaultCountry = programOptions.country.toLowerCase()
const newLabel = ` [new]`
let options = new Collection()
async function main() {
const storage = new Storage()
if (!(await storage.exists(filepath))) {
throw new Error(`File "${filepath}" does not exists`)
}
const parser = new ChannelsParser({ storage })
const parsedChannels = await parser.parse(filepath)
options = parsedChannels.map((channel: Channel) => {
return {
channel,
delete: false
}
})
const dataStorage = new Storage(DATA_DIR)
const channelsContent = await dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
const buffer = new Dictionary()
options.forEach(async (option: { channel: Channel; delete: boolean }) => {
const channel = option.channel
if (channel.xmltv_id) {
if (channel.xmltv_id !== '-') {
buffer.set(`${channel.xmltv_id}/${channel.lang}`, true)
}
return
}
let choices = getOptions(channels, channel)
const question: QuestionCollection = {
name: 'option',
message: `Choose an option:`,
type: 'list',
choices,
pageSize: 10
}
await inquirer.prompt(question).then(async selected => {
switch (selected.option) {
case 'Overwrite':
const input = await getInput(channel)
channel.xmltv_id = input.xmltv_id
break
case 'Skip':
channel.xmltv_id = '-'
break
default:
const [, xmltv_id] = selected.option
.replace(/ \[.*\]/, '')
.split('|')
.map((i: string) => i.trim().replace(newLabel, ''))
channel.xmltv_id = xmltv_id
break
}
const found = buffer.has(`${channel.xmltv_id}/${channel.lang}`)
if (found) {
const question: QuestionCollection = {
name: 'option',
message: `"${channel.xmltv_id}" already on the list. Choose an option:`,
type: 'list',
choices: ['Skip', 'Add', 'Delete'],
pageSize: 5
}
await inquirer.prompt(question).then(async selected => {
switch (selected.option) {
case 'Skip':
channel.xmltv_id = '-'
break
case 'Delete':
option.delete = true
break
default:
break
}
})
} else {
if (channel.xmltv_id !== '-') {
buffer.set(`${channel.xmltv_id}/${channel.lang}`, true)
}
}
})
})
}
main()
function save() {
const logger = new Logger()
const storage = new Storage()
if (!storage.existsSync(filepath)) return
const channels = options
.filter((option: { channel: Channel; delete: boolean }) => !option.delete)
.map((option: { channel: Channel; delete: boolean }) => option.channel)
const xml = new XML(channels)
storage.saveSync(filepath, xml.toString())
logger.info(`\nFile '${filepath}' successfully saved`)
}
nodeCleanup(() => {
save()
})
async function getInput(channel: Channel) {
const name = channel.name.trim()
const input = await inquirer.prompt([
{
name: 'xmltv_id',
message: ' ID:',
type: 'input',
default: generateCode(name, defaultCountry)
}
])
return { name, xmltv_id: input['xmltv_id'] }
}
function getOptions(channels: Collection, channel: Channel) {
const channelId = generateCode(channel.name, defaultCountry)
const similar = getSimilar(channels, channelId)
const variants = new Collection()
variants.add(`${channel.name.trim()} | ${channelId}${newLabel}`)
similar.forEach((_channel: ApiChannel) => {
const altNames = _channel.altNames.notEmpty() ? ` (${_channel.altNames.join(',')})` : ''
const closed = _channel.closed ? `[closed:${_channel.closed}]` : ``
const replacedBy = _channel.replacedBy ? `[replaced_by:${_channel.replacedBy}]` : ''
variants.add(`${_channel.name}${altNames} | ${_channel.id} ${closed}${replacedBy}[api]`)
})
variants.add(`Overwrite`)
variants.add(`Skip`)
return variants.all()
}
function getSimilar(channels: Collection, channelId: string) {
const normChannelId = channelId.split('.')[0].slice(0, 8).toLowerCase()
return channels.filter((channel: ApiChannel) =>
channel.id.split('.')[0].toLowerCase().startsWith(normChannelId)
)
}
function generateCode(name: string, country: string) {
const channelId: string = transliterate(name)
.replace(/\+/gi, 'Plus')
.replace(/^\&/gi, 'And')
.replace(/[^a-z\d]+/gi, '')
return `${channelId}.${country}`
}
import { DATA_DIR } from '../../constants'
import { Storage, Collection, Dictionary, Logger } from '@freearhey/core'
import { ChannelsParser, XML, ApiChannel } from '../../core'
import { Channel } from 'epg-grabber'
import { transliterate } from 'transliteration'
import nodeCleanup from 'node-cleanup'
import { program } from 'commander'
import inquirer, { QuestionCollection } from 'inquirer'
program
.argument('<filepath>', 'Path to *.channels.xml file to edit')
.option('-c, --country <name>', 'Default country (ISO 3166 code)', 'US')
.parse(process.argv)
const filepath = program.args[0]
const programOptions = program.opts()
const defaultCountry = programOptions.country.toLowerCase()
const newLabel = ' [new]'
let options = new Collection()
async function main() {
const storage = new Storage()
if (!(await storage.exists(filepath))) {
throw new Error(`File "${filepath}" does not exists`)
}
const parser = new ChannelsParser({ storage })
const parsedChannels = await parser.parse(filepath)
options = parsedChannels.map((channel: Channel) => {
return {
channel,
delete: false
}
})
const dataStorage = new Storage(DATA_DIR)
const channelsContent = await dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
const buffer = new Dictionary()
options.forEach(async (option: { channel: Channel; delete: boolean }) => {
const channel = option.channel
if (channel.xmltv_id) {
if (channel.xmltv_id !== '-') {
buffer.set(`${channel.xmltv_id}/${channel.lang}`, true)
}
return
}
const choices = getOptions(channels, channel)
const question: QuestionCollection = {
name: 'option',
message: 'Choose an option:',
type: 'list',
choices,
pageSize: 10
}
await inquirer.prompt(question).then(async selected => {
switch (selected.option) {
case 'Overwrite':
const input = await getInput(channel)
channel.xmltv_id = input.xmltv_id
break
case 'Skip':
channel.xmltv_id = '-'
break
default:
const [, xmltv_id] = selected.option
.replace(/ \[.*\]/, '')
.split('|')
.map((i: string) => i.trim().replace(newLabel, ''))
channel.xmltv_id = xmltv_id
break
}
const found = buffer.has(`${channel.xmltv_id}/${channel.lang}`)
if (found) {
const question: QuestionCollection = {
name: 'option',
message: `"${channel.xmltv_id}" already on the list. Choose an option:`,
type: 'list',
choices: ['Skip', 'Add', 'Delete'],
pageSize: 5
}
await inquirer.prompt(question).then(async selected => {
switch (selected.option) {
case 'Skip':
channel.xmltv_id = '-'
break
case 'Delete':
option.delete = true
break
default:
break
}
})
} else {
if (channel.xmltv_id !== '-') {
buffer.set(`${channel.xmltv_id}/${channel.lang}`, true)
}
}
})
})
}
main()
function save() {
const logger = new Logger()
const storage = new Storage()
if (!storage.existsSync(filepath)) return
const channels = options
.filter((option: { channel: Channel; delete: boolean }) => !option.delete)
.map((option: { channel: Channel; delete: boolean }) => option.channel)
const xml = new XML(channels)
storage.saveSync(filepath, xml.toString())
logger.info(`\nFile '${filepath}' successfully saved`)
}
nodeCleanup(() => {
save()
})
async function getInput(channel: Channel) {
const name = channel.name.trim()
const input = await inquirer.prompt([
{
name: 'xmltv_id',
message: ' ID:',
type: 'input',
default: generateCode(name, defaultCountry)
}
])
return { name, xmltv_id: input['xmltv_id'] }
}
function getOptions(channels: Collection, channel: Channel) {
const channelId = generateCode(channel.name, defaultCountry)
const similar = getSimilar(channels, channelId)
const variants = new Collection()
variants.add(`${channel.name.trim()} | ${channelId}${newLabel}`)
similar.forEach((_channel: ApiChannel) => {
const altNames = _channel.altNames.notEmpty() ? ` (${_channel.altNames.join(',')})` : ''
const closed = _channel.closed ? `[closed:${_channel.closed}]` : ''
const replacedBy = _channel.replacedBy ? `[replaced_by:${_channel.replacedBy}]` : ''
variants.add(`${_channel.name}${altNames} | ${_channel.id} ${closed}${replacedBy}[api]`)
})
variants.add('Overwrite')
variants.add('Skip')
return variants.all()
}
function getSimilar(channels: Collection, channelId: string) {
const normChannelId = channelId.split('.')[0].slice(0, 8).toLowerCase()
return channels.filter((channel: ApiChannel) =>
channel.id.split('.')[0].toLowerCase().startsWith(normChannelId)
)
}
function generateCode(name: string, country: string) {
const channelId: string = transliterate(name)
.replace(/\+/gi, 'Plus')
.replace(/^&/gi, 'And')
.replace(/[^a-z\d]+/gi, '')
return `${channelId}.${country}`
}

View file

@ -1,78 +1,78 @@
import chalk from 'chalk'
import libxml, { ValidationError } from 'libxmljs2'
import { program } from 'commander'
import { Logger, Storage, File } from '@freearhey/core'
const xsd = `<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="channels">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="channel"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="channel">
<xs:complexType mixed="true">
<xs:attribute name="site" use="required" type="xs:string"/>
<xs:attribute name="lang" use="required" type="xs:string"/>
<xs:attribute name="site_id" use="required" type="xs:string"/>
<xs:attribute name="xmltv_id" use="required" type="xs:string"/>
<xs:attribute name="logo" type="xs:string"/>
</xs:complexType>
</xs:element>
</xs:schema>`
program
.option(
'-c, --channels <path>',
'Path to channels.xml file to validate',
'sites/**/*.channels.xml'
)
.parse(process.argv)
const options = program.opts()
async function main() {
const logger = new Logger()
const storage = new Storage()
logger.info('options:')
logger.tree(options)
let errors: ValidationError[] = []
let files: string[] = await storage.list(options.channels)
for (const filepath of files) {
const file = new File(filepath)
if (file.extension() !== 'xml') continue
const xml = await storage.load(filepath)
let localErrors: ValidationError[] = []
const xsdDoc = libxml.parseXml(xsd)
const doc = libxml.parseXml(xml)
if (!doc.validate(xsdDoc)) {
localErrors = doc.validationErrors
}
if (localErrors.length) {
console.log(`\n${chalk.underline(filepath)}`)
localErrors.forEach((error: ValidationError) => {
const position = `${error.line}:${error.column}`
console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`)
})
errors = errors.concat(localErrors)
}
}
if (errors.length) {
console.log(chalk.red(`\n${errors.length} error(s)`))
process.exit(1)
}
}
main()
import chalk from 'chalk'
import libxml, { ValidationError } from 'libxmljs2'
import { program } from 'commander'
import { Logger, Storage, File } from '@freearhey/core'
const xsd = `<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="channels">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="channel"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="channel">
<xs:complexType mixed="true">
<xs:attribute name="site" use="required" type="xs:string"/>
<xs:attribute name="lang" use="required" type="xs:string"/>
<xs:attribute name="site_id" use="required" type="xs:string"/>
<xs:attribute name="xmltv_id" use="required" type="xs:string"/>
<xs:attribute name="logo" type="xs:string"/>
</xs:complexType>
</xs:element>
</xs:schema>`
program
.option(
'-c, --channels <path>',
'Path to channels.xml file to validate',
'sites/**/*.channels.xml'
)
.parse(process.argv)
const options = program.opts()
async function main() {
const logger = new Logger()
const storage = new Storage()
logger.info('options:')
logger.tree(options)
let errors: ValidationError[] = []
const files: string[] = await storage.list(options.channels)
for (const filepath of files) {
const file = new File(filepath)
if (file.extension() !== 'xml') continue
const xml = await storage.load(filepath)
let localErrors: ValidationError[] = []
const xsdDoc = libxml.parseXml(xsd)
const doc = libxml.parseXml(xml)
if (!doc.validate(xsdDoc)) {
localErrors = doc.validationErrors
}
if (localErrors.length) {
console.log(`\n${chalk.underline(filepath)}`)
localErrors.forEach((error: ValidationError) => {
const position = `${error.line}:${error.column}`
console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`)
})
errors = errors.concat(localErrors)
}
}
if (errors.length) {
console.log(chalk.red(`\n${errors.length} error(s)`))
process.exit(1)
}
}
main()

View file

@ -1,81 +1,85 @@
import { Logger, File, Collection, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core'
import { Channel } from 'epg-grabber'
import { Command, OptionValues } from 'commander'
import path from 'path'
const program = new Command()
program
.requiredOption('-c, --config <config>', 'Config file')
.option('-s, --set [args...]', 'Set custom arguments')
.option('-o, --output <output>', 'Output file')
.option('--clean', 'Delete the previous *.channels.xml if exists')
.parse(process.argv)
type ParseOptions = {
config: string
set?: string
output?: string
clean?: boolean
}
const options: ParseOptions = program.opts()
async function main() {
const storage = new Storage()
const parser = new ChannelsParser({ storage })
const logger = new Logger()
const file = new File(options.config)
const dir = file.dirname()
const config = require(path.resolve(options.config))
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
let channels = new Collection()
if (!options.clean && (await storage.exists(outputFilepath))) {
channels = await parser.parse(outputFilepath)
}
const args: {
[key: string]: any
} = {}
if (Array.isArray(options.set)) {
options.set.forEach((arg: string) => {
const [key, value] = arg.split(':')
args[key] = value
})
}
let parsedChannels = config.channels(args)
if (isPromise(parsedChannels)) {
parsedChannels = await parsedChannels
}
parsedChannels = parsedChannels.map((channel: Channel) => {
channel.site = config.site
return channel
})
channels = channels
.mergeBy(
new Collection(parsedChannels),
(channel: Channel) => channel.site_id.toString() + channel.lang
)
.orderBy([
(channel: Channel) => channel.lang,
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'),
(channel: Channel) => channel.site_id
])
const xml = new XML(channels)
await storage.save(outputFilepath, xml.toString())
logger.info(`File '${outputFilepath}' successfully saved`)
}
main()
function isPromise(promise: any) {
return !!promise && typeof promise.then === 'function'
}
import { Logger, File, Collection, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core'
import { Channel } from 'epg-grabber'
import { Command } from 'commander'
import path from 'path'
const program = new Command()
program
.requiredOption('-c, --config <config>', 'Config file')
.option('-s, --set [args...]', 'Set custom arguments')
.option('-o, --output <output>', 'Output file')
.option('--clean', 'Delete the previous *.channels.xml if exists')
.parse(process.argv)
type ParseOptions = {
config: string
set?: string
output?: string
clean?: boolean
}
const options: ParseOptions = program.opts()
async function main() {
const storage = new Storage()
const parser = new ChannelsParser({ storage })
const logger = new Logger()
const file = new File(options.config)
const dir = file.dirname()
const config = require(path.resolve(options.config))
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
let channels = new Collection()
if (!options.clean && (await storage.exists(outputFilepath))) {
channels = await parser.parse(outputFilepath)
}
const args: {
[key: string]: string
} = {}
if (Array.isArray(options.set)) {
options.set.forEach((arg: string) => {
const [key, value] = arg.split(':')
args[key] = value
})
}
let parsedChannels = config.channels(args)
if (isPromise(parsedChannels)) {
parsedChannels = await parsedChannels
}
parsedChannels = parsedChannels.map((channel: Channel) => {
channel.site = config.site
return channel
})
channels = channels
.mergeBy(
new Collection(parsedChannels),
(channel: Channel) => channel.site_id.toString() + channel.lang
)
.orderBy([
(channel: Channel) => channel.lang,
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'),
(channel: Channel) => channel.site_id
])
const xml = new XML(channels)
await storage.save(outputFilepath, xml.toString())
logger.info(`File '${outputFilepath}' successfully saved`)
}
main()
function isPromise(promise: object[] | Promise<object[]>) {
return (
!!promise &&
typeof promise === 'object' &&
typeof (promise as Promise<object[]>).then === 'function'
)
}

View file

@ -1,95 +1,95 @@
import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core'
import { ChannelsParser, ApiChannel } from '../../core'
import { program } from 'commander'
import chalk from 'chalk'
import langs from 'langs'
import { DATA_DIR } from '../../constants'
import { Channel } from 'epg-grabber'
program
.option(
'-c, --channels <path>',
'Path to channels.xml file to validate',
'sites/**/*.channels.xml'
)
.parse(process.argv)
const options = program.opts()
type ValidationError = {
type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang'
name: string
lang?: string
xmltv_id?: string
site_id?: string
logo?: string
}
async function main() {
const logger = new Logger()
logger.info('options:')
logger.tree(options)
const parser = new ChannelsParser({ storage: new Storage() })
const dataStorage = new Storage(DATA_DIR)
const channelsContent = await dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
let totalFiles = 0
let totalErrors = 0
const storage = new Storage()
let files: string[] = await storage.list(options.channels)
for (const filepath of files) {
const file = new File(filepath)
if (file.extension() !== 'xml') continue
const parsedChannels = await parser.parse(filepath)
const bufferById = new Dictionary()
const bufferBySiteId = new Dictionary()
const errors: ValidationError[] = []
parsedChannels.forEach((channel: Channel) => {
const bufferId: string = `${channel.xmltv_id}:${channel.lang}`
if (bufferById.missing(bufferId)) {
bufferById.set(bufferId, true)
} else {
errors.push({ type: 'duplicate', ...channel })
totalErrors++
}
const bufferSiteId: string = `${channel.site_id}:${channel.lang}`
if (bufferBySiteId.missing(bufferSiteId)) {
bufferBySiteId.set(bufferSiteId, true)
} else {
errors.push({ type: 'duplicate', ...channel })
totalErrors++
}
if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) {
errors.push({ type: 'wrong_xmltv_id', ...channel })
totalErrors++
}
if (!langs.where('1', channel.lang)) {
errors.push({ type: 'wrong_lang', ...channel })
totalErrors++
}
})
if (errors.length) {
console.log(chalk.underline(filepath))
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
console.log()
totalFiles++
}
}
if (totalErrors > 0) {
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
process.exit(1)
}
}
main()
import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core'
import { ChannelsParser, ApiChannel } from '../../core'
import { program } from 'commander'
import chalk from 'chalk'
import langs from 'langs'
import { DATA_DIR } from '../../constants'
import { Channel } from 'epg-grabber'
program
.option(
'-c, --channels <path>',
'Path to channels.xml file to validate',
'sites/**/*.channels.xml'
)
.parse(process.argv)
const options = program.opts()
type ValidationError = {
type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang'
name: string
lang?: string
xmltv_id?: string
site_id?: string
logo?: string
}
async function main() {
const logger = new Logger()
logger.info('options:')
logger.tree(options)
const parser = new ChannelsParser({ storage: new Storage() })
const dataStorage = new Storage(DATA_DIR)
const channelsContent = await dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
let totalFiles = 0
let totalErrors = 0
const storage = new Storage()
const files: string[] = await storage.list(options.channels)
for (const filepath of files) {
const file = new File(filepath)
if (file.extension() !== 'xml') continue
const parsedChannels = await parser.parse(filepath)
const bufferById = new Dictionary()
const bufferBySiteId = new Dictionary()
const errors: ValidationError[] = []
parsedChannels.forEach((channel: Channel) => {
const bufferId: string = `${channel.xmltv_id}:${channel.lang}`
if (bufferById.missing(bufferId)) {
bufferById.set(bufferId, true)
} else {
errors.push({ type: 'duplicate', ...channel })
totalErrors++
}
const bufferSiteId: string = `${channel.site_id}:${channel.lang}`
if (bufferBySiteId.missing(bufferSiteId)) {
bufferBySiteId.set(bufferSiteId, true)
} else {
errors.push({ type: 'duplicate', ...channel })
totalErrors++
}
if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) {
errors.push({ type: 'wrong_xmltv_id', ...channel })
totalErrors++
}
if (!langs.where('1', channel.lang)) {
errors.push({ type: 'wrong_lang', ...channel })
totalErrors++
}
})
if (errors.length) {
console.log(chalk.underline(filepath))
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
console.log()
totalFiles++
}
}
if (totalErrors > 0) {
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
process.exit(1)
}
}
main()

View file

@ -1,117 +1,117 @@
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
import { program } from 'commander'
import { CronJob } from 'cron'
import { QueueCreator, Job, ChannelsParser } from '../../core'
import { Channel } from 'epg-grabber'
import path from 'path'
import { SITES_DIR } from '../../constants'
program
.option('-s, --site <name>', 'Name of the site to parse')
.option(
'-c, --channels <path>',
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
)
.option('-o, --output <path>', 'Path to output file', 'guide.xml')
.option('-l, --lang <code>', 'Filter channels by language (ISO 639-2 code)')
.option('-t, --timeout <milliseconds>', 'Override the default timeout for each request')
.option(
'--days <days>',
'Override the number of days for which the program will be loaded (defaults to the value from the site config)',
value => parseInt(value)
)
.option(
'--maxConnections <number>',
'Limit on the number of concurrent requests',
value => parseInt(value),
1
)
.option('--cron <expression>', 'Schedule a script run (example: "0 0 * * *")')
.option('--gzip', 'Create a compressed version of the guide as well', false)
.parse(process.argv)
export type GrabOptions = {
site?: string
channels?: string
output: string
gzip: boolean
maxConnections: number
timeout?: string
lang?: string
days?: number
cron?: string
}
const options: GrabOptions = program.opts()
async function main() {
if (!options.site && !options.channels)
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
const logger = new Logger()
logger.start('staring...')
logger.info('config:')
logger.tree(options)
logger.info(`loading channels...`)
const storage = new Storage()
const parser = new ChannelsParser({ storage })
let files: string[] = []
if (options.site) {
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
pattern = pattern.replace(/\\/g, '/')
files = await storage.list(pattern)
} else if (options.channels) {
files = await storage.list(options.channels)
}
let parsedChannels = new Collection()
for (let filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath))
}
if (options.lang) {
parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang)
}
logger.info(` found ${parsedChannels.count()} channels`)
logger.info('creating queue...')
const queueCreator = new QueueCreator({
parsedChannels,
logger,
options
})
const queue = await queueCreator.create()
logger.info(` added ${queue.size()} items`)
const job = new Job({
queue,
logger,
options
})
let runIndex = 1
if (options.cron) {
const cronJob = new CronJob(options.cron, async () => {
logger.info(`run #${runIndex}:`)
const timer = new Timer()
timer.start()
await job.run()
runIndex++
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
})
cronJob.start()
} else {
logger.info(`run #${runIndex}:`)
const timer = new Timer()
timer.start()
await job.run()
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
}
logger.info('finished')
}
main()
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
import { program } from 'commander'
import { CronJob } from 'cron'
import { QueueCreator, Job, ChannelsParser } from '../../core'
import { Channel } from 'epg-grabber'
import path from 'path'
import { SITES_DIR } from '../../constants'
program
.option('-s, --site <name>', 'Name of the site to parse')
.option(
'-c, --channels <path>',
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
)
.option('-o, --output <path>', 'Path to output file', 'guide.xml')
.option('-l, --lang <code>', 'Filter channels by language (ISO 639-2 code)')
.option('-t, --timeout <milliseconds>', 'Override the default timeout for each request')
.option(
'--days <days>',
'Override the number of days for which the program will be loaded (defaults to the value from the site config)',
value => parseInt(value)
)
.option(
'--maxConnections <number>',
'Limit on the number of concurrent requests',
value => parseInt(value),
1
)
.option('--cron <expression>', 'Schedule a script run (example: "0 0 * * *")')
.option('--gzip', 'Create a compressed version of the guide as well', false)
.parse(process.argv)
export type GrabOptions = {
site?: string
channels?: string
output: string
gzip: boolean
maxConnections: number
timeout?: string
lang?: string
days?: number
cron?: string
}
const options: GrabOptions = program.opts()
async function main() {
if (!options.site && !options.channels)
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
const logger = new Logger()
logger.start('staring...')
logger.info('config:')
logger.tree(options)
logger.info('loading channels...')
const storage = new Storage()
const parser = new ChannelsParser({ storage })
let files: string[] = []
if (options.site) {
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
pattern = pattern.replace(/\\/g, '/')
files = await storage.list(pattern)
} else if (options.channels) {
files = await storage.list(options.channels)
}
let parsedChannels = new Collection()
for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath))
}
if (options.lang) {
parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang)
}
logger.info(` found ${parsedChannels.count()} channels`)
logger.info('creating queue...')
const queueCreator = new QueueCreator({
parsedChannels,
logger,
options
})
const queue = await queueCreator.create()
logger.info(` added ${queue.size()} items`)
const job = new Job({
queue,
logger,
options
})
let runIndex = 1
if (options.cron) {
const cronJob = new CronJob(options.cron, async () => {
logger.info(`run #${runIndex}:`)
const timer = new Timer()
timer.start()
await job.run()
runIndex++
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
})
cronJob.start()
} else {
logger.info(`run #${runIndex}:`)
const timer = new Timer()
timer.start()
await job.run()
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
}
logger.info('finished')
}
main()

View file

@ -1,4 +1,4 @@
export const SITES_DIR = process.env.SITES_DIR || './sites'
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
export const CURR_DATE = process.env.CURR_DATE || new Date().toISOString()
export const SITES_DIR = process.env.SITES_DIR || './sites'
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
export const CURR_DATE = process.env.CURR_DATE || new Date().toISOString()

View file

@ -1,79 +1,79 @@
import { Collection } from '@freearhey/core'
type ApiChannelProps = {
id: string
name: string
alt_names: string[]
network: string
owners: string[]
country: string
subdivision: string
city: string
broadcast_area: string[]
languages: string[]
categories: string[]
is_nsfw: boolean
launched: string
closed: string
replaced_by: string
website: string
logo: string
}
export class ApiChannel {
id: string
name: string
altNames: Collection
network: string
owners: Collection
country: string
subdivision: string
city: string
broadcastArea: Collection
languages: Collection
categories: Collection
isNSFW: boolean
launched: string
closed: string
replacedBy: string
website: string
logo: string
constructor({
id,
name,
alt_names,
network,
owners,
country,
subdivision,
city,
broadcast_area,
languages,
categories,
is_nsfw,
launched,
closed,
replaced_by,
website,
logo
}: ApiChannelProps) {
this.id = id
this.name = name
this.altNames = new Collection(alt_names)
this.network = network
this.owners = new Collection(owners)
this.country = country
this.subdivision = subdivision
this.city = city
this.broadcastArea = new Collection(broadcast_area)
this.languages = new Collection(languages)
this.categories = new Collection(categories)
this.isNSFW = is_nsfw
this.launched = launched
this.closed = closed
this.replacedBy = replaced_by
this.website = website
this.logo = logo
}
}
import { Collection } from '@freearhey/core'
type ApiChannelProps = {
id: string
name: string
alt_names: string[]
network: string
owners: string[]
country: string
subdivision: string
city: string
broadcast_area: string[]
languages: string[]
categories: string[]
is_nsfw: boolean
launched: string
closed: string
replaced_by: string
website: string
logo: string
}
export class ApiChannel {
id: string
name: string
altNames: Collection
network: string
owners: Collection
country: string
subdivision: string
city: string
broadcastArea: Collection
languages: Collection
categories: Collection
isNSFW: boolean
launched: string
closed: string
replacedBy: string
website: string
logo: string
constructor({
id,
name,
alt_names,
network,
owners,
country,
subdivision,
city,
broadcast_area,
languages,
categories,
is_nsfw,
launched,
closed,
replaced_by,
website,
logo
}: ApiChannelProps) {
this.id = id
this.name = name
this.altNames = new Collection(alt_names)
this.network = network
this.owners = new Collection(owners)
this.country = country
this.subdivision = subdivision
this.city = city
this.broadcastArea = new Collection(broadcast_area)
this.languages = new Collection(languages)
this.categories = new Collection(categories)
this.isNSFW = is_nsfw
this.launched = launched
this.closed = closed
this.replacedBy = replaced_by
this.website = website
this.logo = logo
}
}

View file

@ -1,59 +1,59 @@
import { Logger, Storage } from '@freearhey/core'
import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios'
import cliProgress, { MultiBar } from 'cli-progress'
import numeral from 'numeral'
export class ApiClient {
progressBar: MultiBar
client: AxiosInstance
storage: Storage
logger: Logger
constructor({ logger }: { logger: Logger }) {
this.logger = logger
this.client = axios.create({
responseType: 'stream'
})
this.storage = new Storage()
this.progressBar = new cliProgress.MultiBar({
stopOnComplete: true,
hideCursor: true,
forceRedraw: true,
barsize: 36,
format(options, params, payload) {
const filename = payload.filename.padEnd(18, ' ')
const barsize = options.barsize || 40
const percent = (params.progress * 100).toFixed(2)
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
const total = numeral(params.total).format('0.0 b')
const completeSize = Math.round(params.progress * barsize)
const incompleteSize = barsize - completeSize
const bar =
options.barCompleteString && options.barIncompleteString
? options.barCompleteString.substr(0, completeSize) +
options.barGlue +
options.barIncompleteString.substr(0, incompleteSize)
: '-'.repeat(barsize)
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
}
})
}
async download(filename: string) {
const stream = await this.storage.createStream(`/temp/data/${filename}`)
const bar = this.progressBar.create(0, 0, { filename })
this.client
.get(`https://iptv-org.github.io/api/${filename}`, {
onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) {
if (total) bar.setTotal(total)
bar.update(loaded, { speed: rate })
}
})
.then((response: AxiosResponse) => {
response.data.pipe(stream)
})
}
}
import { Logger, Storage } from '@freearhey/core'
import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios'
import cliProgress, { MultiBar } from 'cli-progress'
import numeral from 'numeral'
export class ApiClient {
progressBar: MultiBar
client: AxiosInstance
storage: Storage
logger: Logger
constructor({ logger }: { logger: Logger }) {
this.logger = logger
this.client = axios.create({
responseType: 'stream'
})
this.storage = new Storage()
this.progressBar = new cliProgress.MultiBar({
stopOnComplete: true,
hideCursor: true,
forceRedraw: true,
barsize: 36,
format(options, params, payload) {
const filename = payload.filename.padEnd(18, ' ')
const barsize = options.barsize || 40
const percent = (params.progress * 100).toFixed(2)
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
const total = numeral(params.total).format('0.0 b')
const completeSize = Math.round(params.progress * barsize)
const incompleteSize = barsize - completeSize
const bar =
options.barCompleteString && options.barIncompleteString
? options.barCompleteString.substr(0, completeSize) +
options.barGlue +
options.barIncompleteString.substr(0, incompleteSize)
: '-'.repeat(barsize)
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
}
})
}
async download(filename: string) {
const stream = await this.storage.createStream(`/temp/data/${filename}`)
const bar = this.progressBar.create(0, 0, { filename })
this.client
.get(`https://iptv-org.github.io/api/${filename}`, {
onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) {
if (total) bar.setTotal(total)
bar.update(loaded, { speed: rate })
}
})
.then((response: AxiosResponse) => {
response.data.pipe(stream)
})
}
}

View file

@ -1,24 +1,24 @@
import { parseChannels } from 'epg-grabber'
import { Storage, Collection } from '@freearhey/core'
type ChannelsParserProps = {
storage: Storage
}
export class ChannelsParser {
storage: Storage
constructor({ storage }: ChannelsParserProps) {
this.storage = storage
}
async parse(filepath: string) {
let parsedChannels = new Collection()
const content = await this.storage.load(filepath)
const channels = parseChannels(content)
parsedChannels = parsedChannels.concat(new Collection(channels))
return parsedChannels
}
}
import { parseChannels } from 'epg-grabber'
import { Storage, Collection } from '@freearhey/core'
type ChannelsParserProps = {
storage: Storage
}
export class ChannelsParser {
storage: Storage
constructor({ storage }: ChannelsParserProps) {
this.storage = storage
}
async parse(filepath: string) {
let parsedChannels = new Collection()
const content = await this.storage.load(filepath)
const channels = parseChannels(content)
parsedChannels = parsedChannels.concat(new Collection(channels))
return parsedChannels
}
}

View file

@ -1,21 +1,21 @@
import { SiteConfig } from 'epg-grabber'
import _ from 'lodash'
import { pathToFileURL } from 'url'
export class ConfigLoader {
async load(filepath: string): Promise<SiteConfig> {
const fileUrl = pathToFileURL(filepath).toString()
const config = (await import(fileUrl)).default
return _.merge(
{
delay: 0,
maxConnections: 1,
request: {
timeout: 30000
}
},
config
)
}
}
import { SiteConfig } from 'epg-grabber'
import _ from 'lodash'
import { pathToFileURL } from 'url'
export class ConfigLoader {
async load(filepath: string): Promise<SiteConfig> {
const fileUrl = pathToFileURL(filepath).toString()
const config = (await import(fileUrl)).default
return _.merge(
{
delay: 0,
maxConnections: 1,
request: {
timeout: 30000
}
},
config
)
}
}

View file

@ -1,13 +1,13 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = {}
date.getUTC = function (d = null) {
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
return dayjs.utc().startOf('d')
}
module.exports = date
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = {}
date.getUTC = function (d = null) {
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
return dayjs.utc().startOf('d')
}
module.exports = date

View file

@ -1,75 +1,75 @@
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
import { Logger, Collection } from '@freearhey/core'
import { Queue } from './'
import { GrabOptions } from '../commands/epg/grab'
import { TaskQueue, PromisyClass } from 'cwait'
type GrabberProps = {
logger: Logger
queue: Queue
options: GrabOptions
}
export class Grabber {
logger: Logger
queue: Queue
options: GrabOptions
constructor({ logger, queue, options }: GrabberProps) {
this.logger = logger
this.queue = queue
this.options = options
}
async grab(): Promise<{ channels: Collection; programs: Collection }> {
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
const total = this.queue.size()
const channels = new Collection()
let programs = new Collection()
let i = 1
await Promise.all(
this.queue.items().map(
taskQueue.wrap(
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
const { channel, config, date } = queueItem
channels.add(channel)
if (this.options.timeout !== undefined) {
const timeout = parseInt(this.options.timeout)
config.request = { ...config.request, ...{ timeout } }
}
const grabber =
process.env.NODE_ENV === 'test' ? new EPGGrabberMock(config) : new EPGGrabber(config)
const _programs = await grabber.grab(
channel,
date,
(data: GrabCallbackData, error: Error | null) => {
const { programs, date } = data
this.logger.info(
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
channel.xmltv_id
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
)
if (i < total) i++
if (error) {
this.logger.info(` ERR: ${error.message}`)
}
}
)
programs = programs.concat(new Collection(_programs))
}
)
)
)
return { channels, programs }
}
}
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
import { Logger, Collection } from '@freearhey/core'
import { Queue } from './'
import { GrabOptions } from '../commands/epg/grab'
import { TaskQueue, PromisyClass } from 'cwait'
type GrabberProps = {
logger: Logger
queue: Queue
options: GrabOptions
}
export class Grabber {
logger: Logger
queue: Queue
options: GrabOptions
constructor({ logger, queue, options }: GrabberProps) {
this.logger = logger
this.queue = queue
this.options = options
}
async grab(): Promise<{ channels: Collection; programs: Collection }> {
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
const total = this.queue.size()
const channels = new Collection()
let programs = new Collection()
let i = 1
await Promise.all(
this.queue.items().map(
taskQueue.wrap(
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
const { channel, config, date } = queueItem
channels.add(channel)
if (this.options.timeout !== undefined) {
const timeout = parseInt(this.options.timeout)
config.request = { ...config.request, ...{ timeout } }
}
const grabber =
process.env.NODE_ENV === 'test' ? new EPGGrabberMock(config) : new EPGGrabber(config)
const _programs = await grabber.grab(
channel,
date,
(data: GrabCallbackData, error: Error | null) => {
const { programs, date } = data
this.logger.info(
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
channel.xmltv_id
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
)
if (i < total) i++
if (error) {
this.logger.info(` ERR: ${error.message}`)
}
}
)
programs = programs.concat(new Collection(_programs))
}
)
)
)
return { channels, programs }
}
}

View file

@ -1,55 +1,55 @@
import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core'
import { Channel } from 'epg-grabber'
import { XMLTV } from '../core'
import { CURR_DATE } from '../constants'
type GuideProps = {
channels: Collection
programs: Collection
logger: Logger
filepath: string
gzip: boolean
}
export class Guide {
channels: Collection
programs: Collection
logger: Logger
storage: Storage
filepath: string
gzip: boolean
constructor({ channels, programs, logger, filepath, gzip }: GuideProps) {
this.channels = channels
this.programs = programs
this.logger = logger
this.storage = new Storage()
this.filepath = filepath
this.gzip = gzip || false
}
async save() {
const channels = this.channels.uniqBy(
(channel: Channel) => `${channel.xmltv_id}:${channel.site}`
)
const programs = this.programs
const xmltv = new XMLTV({
channels,
programs,
date: new DateTime(CURR_DATE, { zone: 'UTC' })
})
const xmlFilepath = this.filepath
this.logger.info(` saving to "${xmlFilepath}"...`)
await this.storage.save(xmlFilepath, xmltv.toString())
if (this.gzip) {
const zip = new Zip()
const compressed = await zip.compress(xmltv.toString())
const gzFilepath = `${this.filepath}.gz`
this.logger.info(` saving to "${gzFilepath}"...`)
await this.storage.save(gzFilepath, compressed)
}
}
}
import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core'
import { Channel } from 'epg-grabber'
import { XMLTV } from '../core'
import { CURR_DATE } from '../constants'
type GuideProps = {
channels: Collection
programs: Collection
logger: Logger
filepath: string
gzip: boolean
}
export class Guide {
channels: Collection
programs: Collection
logger: Logger
storage: Storage
filepath: string
gzip: boolean
constructor({ channels, programs, logger, filepath, gzip }: GuideProps) {
this.channels = channels
this.programs = programs
this.logger = logger
this.storage = new Storage()
this.filepath = filepath
this.gzip = gzip || false
}
async save() {
const channels = this.channels.uniqBy(
(channel: Channel) => `${channel.xmltv_id}:${channel.site}`
)
const programs = this.programs
const xmltv = new XMLTV({
channels,
programs,
date: new DateTime(CURR_DATE, { zone: 'UTC' })
})
const xmlFilepath = this.filepath
this.logger.info(` saving to "${xmlFilepath}"...`)
await this.storage.save(xmlFilepath, xmltv.toString())
if (this.gzip) {
const zip = new Zip()
const compressed = await zip.compress(xmltv.toString())
const gzFilepath = `${this.filepath}.gz`
this.logger.info(` saving to "${gzFilepath}"...`)
await this.storage.save(gzFilepath, compressed)
}
}
}

View file

@ -1,61 +1,61 @@
import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core'
import { OptionValues } from 'commander'
import { Channel, Program } from 'epg-grabber'
import { Guide } from '.'
type GuideManagerProps = {
options: OptionValues
logger: Logger
channels: Collection
programs: Collection
}
export class GuideManager {
options: OptionValues
storage: Storage
logger: Logger
channels: Collection
programs: Collection
constructor({ channels, programs, logger, options }: GuideManagerProps) {
this.options = options
this.logger = logger
this.channels = channels
this.programs = programs
this.storage = new Storage()
}
async createGuides() {
const pathTemplate = new StringTemplate(this.options.output)
const groupedChannels = this.channels
.orderBy([(channel: Channel) => channel.xmltv_id])
.uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`)
.groupBy((channel: Channel) => {
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
})
const groupedPrograms = this.programs
.orderBy([(program: Program) => program.channel, (program: Program) => program.start])
.groupBy((program: Program) => {
const lang =
program.titles && program.titles.length && program.titles[0].lang
? program.titles[0].lang
: 'en'
return pathTemplate.format({ lang, site: program.site || '' })
})
for (const groupKey of groupedPrograms.keys()) {
const guide = new Guide({
filepath: groupKey,
gzip: this.options.gzip,
channels: new Collection(groupedChannels.get(groupKey)),
programs: new Collection(groupedPrograms.get(groupKey)),
logger: this.logger
})
await guide.save()
}
}
}
import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core'
import { OptionValues } from 'commander'
import { Channel, Program } from 'epg-grabber'
import { Guide } from '.'
type GuideManagerProps = {
options: OptionValues
logger: Logger
channels: Collection
programs: Collection
}
export class GuideManager {
options: OptionValues
storage: Storage
logger: Logger
channels: Collection
programs: Collection
constructor({ channels, programs, logger, options }: GuideManagerProps) {
this.options = options
this.logger = logger
this.channels = channels
this.programs = programs
this.storage = new Storage()
}
async createGuides() {
const pathTemplate = new StringTemplate(this.options.output)
const groupedChannels = this.channels
.orderBy([(channel: Channel) => channel.xmltv_id])
.uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`)
.groupBy((channel: Channel) => {
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
})
const groupedPrograms = this.programs
.orderBy([(program: Program) => program.channel, (program: Program) => program.start])
.groupBy((program: Program) => {
const lang =
program.titles && program.titles.length && program.titles[0].lang
? program.titles[0].lang
: 'en'
return pathTemplate.format({ lang, site: program.site || '' })
})
for (const groupKey of groupedPrograms.keys()) {
const guide = new Guide({
filepath: groupKey,
gzip: this.options.gzip,
channels: new Collection(groupedChannels.get(groupKey)),
programs: new Collection(groupedPrograms.get(groupKey)),
logger: this.logger
})
await guide.save()
}
}
}

View file

@ -1,12 +1,12 @@
export * from './xml'
export * from './channelsParser'
export * from './xmltv'
export * from './configLoader'
export * from './grabber'
export * from './job'
export * from './queue'
export * from './guideManager'
export * from './guide'
export * from './apiChannel'
export * from './apiClient'
export * from './queueCreator'
export * from './xml'
export * from './channelsParser'
export * from './xmltv'
export * from './configLoader'
export * from './grabber'
export * from './job'
export * from './queue'
export * from './guideManager'
export * from './guide'
export * from './apiChannel'
export * from './apiClient'
export * from './queueCreator'

View file

@ -1,34 +1,34 @@
import { Logger } from '@freearhey/core'
import { Queue, Grabber, GuideManager } from '.'
import { GrabOptions } from '../commands/epg/grab'
type JobProps = {
options: GrabOptions
logger: Logger
queue: Queue
}
export class Job {
options: GrabOptions
logger: Logger
grabber: Grabber
constructor({ queue, logger, options }: JobProps) {
this.options = options
this.logger = logger
this.grabber = new Grabber({ logger, queue, options })
}
async run() {
const { channels, programs } = await this.grabber.grab()
const manager = new GuideManager({
channels,
programs,
options: this.options,
logger: this.logger
})
await manager.createGuides()
}
}
import { Logger } from '@freearhey/core'
import { Queue, Grabber, GuideManager } from '.'
import { GrabOptions } from '../commands/epg/grab'
type JobProps = {
options: GrabOptions
logger: Logger
queue: Queue
}
export class Job {
options: GrabOptions
logger: Logger
grabber: Grabber
constructor({ queue, logger, options }: JobProps) {
this.options = options
this.logger = logger
this.grabber = new Grabber({ logger, queue, options })
}
async run() {
const { channels, programs } = await this.grabber.grab()
const manager = new GuideManager({
channels,
programs,
options: this.options,
logger: this.logger
})
await manager.createGuides()
}
}

View file

@ -1,45 +1,45 @@
import { Dictionary } from '@freearhey/core'
import { SiteConfig, Channel } from 'epg-grabber'
export type QueueItem = {
channel: Channel
date: string
config: SiteConfig
error: string | null
}
export class Queue {
_data: Dictionary
constructor() {
this._data = new Dictionary()
}
missing(key: string): boolean {
return this._data.missing(key)
}
add(
key: string,
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
) {
this._data.set(key, {
channel,
date,
config,
error: null
})
}
size(): number {
return Object.values(this._data.data()).length
}
items(): QueueItem[] {
return Object.values(this._data.data()) as QueueItem[]
}
isEmpty(): boolean {
return this.size() === 0
}
}
import { Dictionary } from '@freearhey/core'
import { SiteConfig, Channel } from 'epg-grabber'
export type QueueItem = {
channel: Channel
date: string
config: SiteConfig
error: string | null
}
export class Queue {
_data: Dictionary
constructor() {
this._data = new Dictionary()
}
missing(key: string): boolean {
return this._data.missing(key)
}
add(
key: string,
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
) {
this._data.set(key, {
channel,
date,
config,
error: null
})
}
size(): number {
return Object.values(this._data.data()).length
}
items(): QueueItem[] {
return Object.values(this._data.data()) as QueueItem[]
}
isEmpty(): boolean {
return this.size() === 0
}
}

View file

@ -1,71 +1,71 @@
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './'
import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants'
import { SiteConfig } from 'epg-grabber'
import path from 'path'
import { GrabOptions } from '../commands/epg/grab'
type QueueCreatorProps = {
logger: Logger
options: GrabOptions
parsedChannels: Collection
}
export class QueueCreator {
configLoader: ConfigLoader
logger: Logger
sitesStorage: Storage
dataStorage: Storage
parser: ChannelsParser
parsedChannels: Collection
options: GrabOptions
date: DateTime
constructor({ parsedChannels, logger, options }: QueueCreatorProps) {
this.parsedChannels = parsedChannels
this.logger = logger
this.sitesStorage = new Storage()
this.dataStorage = new Storage(DATA_DIR)
this.parser = new ChannelsParser({ storage: new Storage() })
this.date = new DateTime(CURR_DATE)
this.options = options
this.configLoader = new ConfigLoader()
}
async create(): Promise<Queue> {
const channelsContent = await this.dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
const queue = new Queue()
for (const channel of this.parsedChannels.all()) {
if (!channel.site || !channel.xmltv_id) continue
if (this.options.lang && channel.lang !== this.options.lang) continue
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
const config: SiteConfig = await this.configLoader.load(configPath)
const found: ApiChannel = channels.first(
(_channel: ApiChannel) => _channel.id === channel.xmltv_id
)
if (found) {
channel.logo = found.logo
}
const days = this.options.days || config.days || 1
const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd'))
dates.forEach((date: DateTime) => {
const dateString = date.toJSON()
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
if (queue.missing(key)) {
queue.add(key, {
channel,
date: dateString,
config
})
}
})
}
return queue
}
}
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './'
import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants'
import { SiteConfig } from 'epg-grabber'
import path from 'path'
import { GrabOptions } from '../commands/epg/grab'
type QueueCreatorProps = {
logger: Logger
options: GrabOptions
parsedChannels: Collection
}
export class QueueCreator {
configLoader: ConfigLoader
logger: Logger
sitesStorage: Storage
dataStorage: Storage
parser: ChannelsParser
parsedChannels: Collection
options: GrabOptions
date: DateTime
constructor({ parsedChannels, logger, options }: QueueCreatorProps) {
this.parsedChannels = parsedChannels
this.logger = logger
this.sitesStorage = new Storage()
this.dataStorage = new Storage(DATA_DIR)
this.parser = new ChannelsParser({ storage: new Storage() })
this.date = new DateTime(CURR_DATE)
this.options = options
this.configLoader = new ConfigLoader()
}
async create(): Promise<Queue> {
const channelsContent = await this.dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
const queue = new Queue()
for (const channel of this.parsedChannels.all()) {
if (!channel.site || !channel.xmltv_id) continue
if (this.options.lang && channel.lang !== this.options.lang) continue
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
const config: SiteConfig = await this.configLoader.load(configPath)
const found: ApiChannel = channels.first(
(_channel: ApiChannel) => _channel.id === channel.xmltv_id
)
if (found) {
channel.logo = found.logo
}
const days = this.options.days || config.days || 1
const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd'))
dates.forEach((date: DateTime) => {
const dateString = date.toJSON()
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
if (queue.missing(key)) {
queue.add(key, {
channel,
date: dateString,
config
})
}
})
}
return queue
}
}

View file

@ -1,56 +1,56 @@
import { Collection } from '@freearhey/core'
import { Channel } from 'epg-grabber'
export class XML {
items: Collection
constructor(items: Collection) {
this.items = items
}
toString() {
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.items.forEach((channel: Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id || ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
output += ` <channel site="${channel.site}" lang="${lang}" xmltv_id="${escapeString(
xmltv_id
)}" site_id="${site_id}"${logo}>${escapeString(channel.name)}</channel>\r\n`
})
output += '</channels>\r\n'
return output
}
}
function escapeString(value: string, defaultValue: string = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}
import { Collection } from '@freearhey/core'
import { Channel } from 'epg-grabber'
export class XML {
items: Collection
constructor(items: Collection) {
this.items = items
}
toString() {
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.items.forEach((channel: Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id || ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
output += ` <channel site="${channel.site}" lang="${lang}" xmltv_id="${escapeString(
xmltv_id
)}" site_id="${site_id}"${logo}>${escapeString(channel.name)}</channel>\r\n`
})
output += '</channels>\r\n'
return output
}
}
function escapeString(value: string, defaultValue: string = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}

View file

@ -1,28 +1,28 @@
import { DateTime, Collection } from '@freearhey/core'
import { generateXMLTV } from 'epg-grabber'
type XMLTVProps = {
channels: Collection
programs: Collection
date: DateTime
}
export class XMLTV {
channels: Collection
programs: Collection
date: DateTime
constructor({ channels, programs, date }: XMLTVProps) {
this.channels = channels
this.programs = programs
this.date = date
}
toString() {
return generateXMLTV({
channels: this.channels.all(),
programs: this.programs.all(),
date: this.date.toJSON()
})
}
}
import { DateTime, Collection } from '@freearhey/core'
import { generateXMLTV } from 'epg-grabber'
type XMLTVProps = {
channels: Collection
programs: Collection
date: DateTime
}
export class XMLTV {
channels: Collection
programs: Collection
date: DateTime
constructor({ channels, programs, date }: XMLTVProps) {
this.channels = channels
this.programs = programs
this.date = date
}
toString() {
return generateXMLTV({
channels: this.channels.all(),
programs: this.programs.all(),
date: this.date.toJSON()
})
}
}

View file

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

View file

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

View file

@ -1,57 +1,57 @@
// npm run grab -- --site=9tv.co.il
const { parser, url } = require('./9tv.co.il.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'Channel9.il'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
)
})
it('can parse response', () => {
const content =
'<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.&#160;</div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>'
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-06T04:30:00.000Z',
stop: '2022-03-06T07:10:00.000Z',
title: 'Слепая',
icon: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
description:
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
},
{
start: '2022-03-06T07:10:00.000Z',
stop: '2022-03-06T08:10:00.000Z',
icon: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
title: 'Орел и решка. Морской сезон',
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=9tv.co.il
const { parser, url } = require('./9tv.co.il.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'Channel9.il'
}
it('can generate valid url', () => {
expect(url({ date })).toBe(
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
)
})
it('can parse response', () => {
const content =
'<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.&#160;</div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>'
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-06T04:30:00.000Z',
stop: '2022-03-06T07:10:00.000Z',
title: 'Слепая',
icon: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
description:
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
},
{
start: '2022-03-06T07:10:00.000Z',
stop: '2022-03-06T08:10:00.000Z',
icon: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
title: 'Орел и решка. Морской сезон',
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})

View file

@ -1,77 +1,77 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'abc.net.au',
days: 3,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url({ date }) {
return `https://epg.abctv.net.au/processed/Sydney_${date.format('YYYY-MM-DD')}.json`
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
programs.push({
title: item.title,
sub_title: item.episode_title,
category: item.genres,
description: item.description,
season: parseSeason(item),
episode: parseEpisode(item),
rating: parseRating(item),
icon: parseIcon(item),
start: parseTime(item.start_time),
stop: parseTime(item.end_time)
})
})
return programs
}
}
function parseItems(content, channel) {
try {
const data = JSON.parse(content)
if (!data) return []
if (!Array.isArray(data.schedule)) return []
const channelData = data.schedule.find(i => i.channel == channel.site_id)
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
} catch (err) {
return []
}
}
function parseSeason(item) {
return item.series_num || null
}
function parseEpisode(item) {
return item.episode_num || null
}
function parseTime(time) {
return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney')
}
function parseIcon(item) {
return item.image_file
? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}`
: null
}
function parseRating(item) {
return item.rating
? {
system: 'ACB',
value: item.rating
}
: null
}
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'abc.net.au',
days: 3,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url({ date }) {
return `https://epg.abctv.net.au/processed/Sydney_${date.format('YYYY-MM-DD')}.json`
},
parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
programs.push({
title: item.title,
sub_title: item.episode_title,
category: item.genres,
description: item.description,
season: parseSeason(item),
episode: parseEpisode(item),
rating: parseRating(item),
icon: parseIcon(item),
start: parseTime(item.start_time),
stop: parseTime(item.end_time)
})
})
return programs
}
}
function parseItems(content, channel) {
try {
const data = JSON.parse(content)
if (!data) return []
if (!Array.isArray(data.schedule)) return []
const channelData = data.schedule.find(i => i.channel == channel.site_id)
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
} catch (err) {
return []
}
}
function parseSeason(item) {
return item.series_num || null
}
function parseEpisode(item) {
return item.episode_num || null
}
function parseTime(time) {
return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney')
}
function parseIcon(item) {
return item.image_file
? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}`
: null
}
function parseRating(item) {
return item.rating
? {
system: 'ACB',
value: item.rating
}
: null
}

View file

@ -1,56 +1,56 @@
// npm run grab -- --site=abc.net.au
const { parser, url } = require('./abc.net.au.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ABC1',
xmltv_id: 'ABCTV.au'
}
it('can generate valid url', () => {
expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json')
})
it('can parse response', () => {
const content =
'{"date":"2022-12-22","region":"Sydney","schedule":[{"channel":"ABC1","listing":[{"consumer_advice":"Adult Themes, Drug Use, Violence","rating":"M","show_id":912747,"repeat":true,"description":"When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?","title":"Silent Witness","crid":"ZW2178A004S00","start_time":"2022-12-22T00:46:00","series-crid":"ZW2178A","live":false,"captioning":true,"show_type":"Episode","series_num":22,"episode_title":"Lift Up Your Hearts (part Two)","length":58,"onair_title":"Silent Witness","end_time":"2022-12-22T01:44:00","genres":["Entertainment"],"image_file":"ZW2178A004S00_460.jpg","prog_slug":"silent-witness","episode_num":4}]}]}'
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'Silent Witness',
sub_title: 'Lift Up Your Hearts (part Two)',
description:
'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?',
category: ['Entertainment'],
rating: {
system: 'ACB',
value: 'M'
},
season: 22,
episode: 4,
icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg',
start: '2022-12-21T13:46:00.000Z',
stop: '2022-12-21T14:44:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser(
{
content:
'<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>processed/Sydney_2023-01-17.json</Key><RequestId>6MRHX5TJ12X39B3Y</RequestId><HostId>59rH6XRMrmkFywg8Kv58iqpI6O1fuOCuEbKa1HRRYa4buByXMBTvAhz8zuAK7X5D+ZN9ZuWxyGs=</HostId></Error>'
},
channel
)
expect(result).toMatchObject([])
})
// npm run grab -- --site=abc.net.au
const { parser, url } = require('./abc.net.au.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ABC1',
xmltv_id: 'ABCTV.au'
}
it('can generate valid url', () => {
expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json')
})
it('can parse response', () => {
const content =
'{"date":"2022-12-22","region":"Sydney","schedule":[{"channel":"ABC1","listing":[{"consumer_advice":"Adult Themes, Drug Use, Violence","rating":"M","show_id":912747,"repeat":true,"description":"When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?","title":"Silent Witness","crid":"ZW2178A004S00","start_time":"2022-12-22T00:46:00","series-crid":"ZW2178A","live":false,"captioning":true,"show_type":"Episode","series_num":22,"episode_title":"Lift Up Your Hearts (part Two)","length":58,"onair_title":"Silent Witness","end_time":"2022-12-22T01:44:00","genres":["Entertainment"],"image_file":"ZW2178A004S00_460.jpg","prog_slug":"silent-witness","episode_num":4}]}]}'
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'Silent Witness',
sub_title: 'Lift Up Your Hearts (part Two)',
description:
'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?',
category: ['Entertainment'],
rating: {
system: 'ACB',
value: 'M'
},
season: 22,
episode: 4,
icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg',
start: '2022-12-21T13:46:00.000Z',
stop: '2022-12-21T14:44:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser(
{
content:
'<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>processed/Sydney_2023-01-17.json</Key><RequestId>6MRHX5TJ12X39B3Y</RequestId><HostId>59rH6XRMrmkFywg8Kv58iqpI6O1fuOCuEbKa1HRRYa4buByXMBTvAhz8zuAK7X5D+ZN9ZuWxyGs=</HostId></Error>'
},
channel
)
expect(result).toMatchObject([])
})

View file

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

View file

@ -1,62 +1,62 @@
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_se.channels.xml --set=country:se --set=lang:sv
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_fi.channels.xml --set=country:fi --set=lang:fi
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_no.channels.xml --set=country:no --set=lang:no
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_dk.channels.xml --set=country:dk --set=lang:da
// npm run grab -- --site=allente.se
const { parser, url } = require('./allente.se.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'se#0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17')
})
it('can generate valid url for different country', () => {
const dkChannel = { site_id: 'dk#0148' }
expect(url({ date, channel: dkChannel })).toBe(
'https://cs-vcb.allente.dk/epg/events?date=2021-11-17'
)
})
it('can parse response', () => {
const content =
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
icon: 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
})
expect(result).toMatchObject([])
})
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_se.channels.xml --set=country:se --set=lang:sv
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_fi.channels.xml --set=country:fi --set=lang:fi
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_no.channels.xml --set=country:no --set=lang:no
// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_dk.channels.xml --set=country:dk --set=lang:da
// npm run grab -- --site=allente.se
const { parser, url } = require('./allente.se.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'se#0148',
xmltv_id: 'SVT1.se'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17')
})
it('can generate valid url for different country', () => {
const dkChannel = { site_id: 'dk#0148' }
expect(url({ date, channel: dkChannel })).toBe(
'https://cs-vcb.allente.dk/epg/events?date=2021-11-17'
)
})
it('can parse response', () => {
const content =
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia',
category: ['other'],
description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
icon: 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440',
season: 4,
episode: 1
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}'
})
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

@ -1,82 +1,82 @@
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'arianaafgtv.com',
days: 2,
url: 'https://www.arianaafgtv.com/index.html',
parser({ content, date }) {
const programs = []
const items = parseItems(content, date)
items.forEach(item => {
const title = item.title
const start = parseStart(item, date)
const stop = parseStop(item, date)
programs.push({
title,
start,
stop
})
})
return programs
}
}
function parseStop(item, date) {
const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}`
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
}
function parseStart(item, date) {
const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}`
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
}
function parseItems(content, date) {
const $ = cheerio.load(content)
const dayOfWeek = date.format('dddd')
const column = $('.H4')
.filter((i, el) => {
return $(el).text() === dayOfWeek
})
.first()
.parent()
const rows = column
.find('.Paragraph')
.map((i, el) => {
return $(el).html()
})
.toArray()
.map(r => (r === '&nbsp;' ? '|' : r))
.join(' ')
.split('|')
const items = []
rows.forEach(row => {
row = row.trim()
if (row) {
const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi)
if (!found) return
const time = found[0]
let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1]
start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1]
end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
const title = row.replace(time, '').replace('&nbsp;', '').trim()
items.push({ start, end, title })
}
})
return items
}
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'arianaafgtv.com',
days: 2,
url: 'https://www.arianaafgtv.com/index.html',
parser({ content, date }) {
const programs = []
const items = parseItems(content, date)
items.forEach(item => {
const title = item.title
const start = parseStart(item, date)
const stop = parseStop(item, date)
programs.push({
title,
start,
stop
})
})
return programs
}
}
function parseStop(item, date) {
const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}`
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
}
function parseStart(item, date) {
const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}`
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
}
function parseItems(content, date) {
const $ = cheerio.load(content)
const dayOfWeek = date.format('dddd')
const column = $('.H4')
.filter((i, el) => {
return $(el).text() === dayOfWeek
})
.first()
.parent()
const rows = column
.find('.Paragraph')
.map((i, el) => {
return $(el).html()
})
.toArray()
.map(r => (r === '&nbsp;' ? '|' : r))
.join(' ')
.split('|')
const items = []
rows.forEach(row => {
row = row.trim()
if (row) {
const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi)
if (!found) return
const time = found[0]
let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1]
start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1]
end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
const title = row.replace(time, '').replace('&nbsp;', '').trim()
items.push({ start, end, title })
}
})
return items
}

View file

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

View file

@ -1,61 +1,61 @@
// npm run grab -- --site=arianatelevision.com
const { parser, url } = require('./arianatelevision.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'ArianaTVNational.af'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.arianatelevision.com/program-schedule/')
})
it('can parse response', () => {
const content =
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>'
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-27T02:30:00.000Z',
stop: '2021-11-27T03:00:00.000Z',
title: 'City Report'
},
{
start: '2021-11-27T03:00:00.000Z',
stop: '2021-11-27T10:30:00.000Z',
title: 'ICC T20 Highlights'
},
{
start: '2021-11-27T10:30:00.000Z',
stop: '2021-11-28T02:00:00.000Z',
title: 'ICC T20 World Cup'
},
{
start: '2021-11-28T02:00:00.000Z',
stop: '2021-11-28T02:30:00.000Z',
title: 'Quran and Hadis'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content:
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>'
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=arianatelevision.com
const { parser, url } = require('./arianatelevision.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '#',
xmltv_id: 'ArianaTVNational.af'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.arianatelevision.com/program-schedule/')
})
it('can parse response', () => {
const content =
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>'
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-27T02:30:00.000Z',
stop: '2021-11-27T03:00:00.000Z',
title: 'City Report'
},
{
start: '2021-11-27T03:00:00.000Z',
stop: '2021-11-27T10:30:00.000Z',
title: 'ICC T20 Highlights'
},
{
start: '2021-11-27T10:30:00.000Z',
stop: '2021-11-28T02:00:00.000Z',
title: 'ICC T20 World Cup'
},
{
start: '2021-11-28T02:00:00.000Z',
stop: '2021-11-28T02:30:00.000Z',
title: 'Quran and Hadis'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content:
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>'
})
expect(result).toMatchObject([])
})

View file

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

View file

@ -1,74 +1,74 @@
// npm run grab -- --site=arirang.com
// npx jest arirang.com.test.js
const { url, parser } = require('./arirang.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.tz('2023-08-25', 'Asia/Seoul').startOf('d')
const channel = {
xmltv_id: 'ArirangWorld.kr',
site_id: 'CH_W',
name: 'Arirang World',
lang: 'en',
logo: 'https://i.imgur.com/5Aoithj.png'
}
const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8')
const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8')
const context = { channel: channel, content: content, date: date }
it('can generate valid url', () => {
expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy')
})
it('can handle empty guide', async () => {
const results = await parser({ channel: channel, content: '', date: date })
expect(results).toMatchObject([])
})
it('can parse response', async () => {
axios.post.mockImplementation((url, data) => {
if (
url === 'https://www.arirang.com/v1.0/open/external/proxy' &&
JSON.stringify(data) ===
JSON.stringify({
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
method: 'POST',
headers: {},
body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20230825', planNo: '1' } } }
})
) {
return Promise.resolve({
data: JSON.parse(content)
})
} else if (
url === 'https://www.arirang.com/v1.0/open/program/detail' &&
JSON.stringify(data) === JSON.stringify({ bis_program_code: '2023004T' })
) {
return Promise.resolve({
data: JSON.parse(programDetail)
})
} else {
return Promise.resolve({
data: ''
})
}
})
const results = await parser(context)
expect(results[0]).toMatchObject({
title: 'WITHIN THE FRAME [R]',
start: dayjs.tz(date, 'Asia/Seoul'),
stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'),
icon: 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202308/2080840096998752900.png',
description: 'NEWS',
category: 'Current Affairs'
})
})
// npm run grab -- --site=arirang.com
// npx jest arirang.com.test.js
const { url, parser } = require('./arirang.com.config.js')
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.tz('2023-08-25', 'Asia/Seoul').startOf('d')
const channel = {
xmltv_id: 'ArirangWorld.kr',
site_id: 'CH_W',
name: 'Arirang World',
lang: 'en',
logo: 'https://i.imgur.com/5Aoithj.png'
}
const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8')
const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8')
const context = { channel: channel, content: content, date: date }
it('can generate valid url', () => {
expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy')
})
it('can handle empty guide', async () => {
const results = await parser({ channel: channel, content: '', date: date })
expect(results).toMatchObject([])
})
it('can parse response', async () => {
axios.post.mockImplementation((url, data) => {
if (
url === 'https://www.arirang.com/v1.0/open/external/proxy' &&
JSON.stringify(data) ===
JSON.stringify({
address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do',
method: 'POST',
headers: {},
body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20230825', planNo: '1' } } }
})
) {
return Promise.resolve({
data: JSON.parse(content)
})
} else if (
url === 'https://www.arirang.com/v1.0/open/program/detail' &&
JSON.stringify(data) === JSON.stringify({ bis_program_code: '2023004T' })
) {
return Promise.resolve({
data: JSON.parse(programDetail)
})
} else {
return Promise.resolve({
data: ''
})
}
})
const results = await parser(context)
expect(results[0]).toMatchObject({
title: 'WITHIN THE FRAME [R]',
start: dayjs.tz(date, 'Asia/Seoul'),
stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'),
icon: 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202308/2080840096998752900.png',
description: 'NEWS',
category: 'Current Affairs'
})
})

View file

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

View file

@ -1,67 +1,67 @@
// npm run grab -- --site=artonline.tv
const { parser, url, request } = require('./artonline.tv.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const channel = {
site_id: 'Aflam2',
xmltv_id: 'ARTAflam2.sa'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'content-type': 'application/x-www-form-urlencoded'
})
})
it('can generate valid request data for today', () => {
const date = dayjs.utc().startOf('d')
const data = request.data({ date })
expect(data.get('objId')).toBe('0')
})
it('can generate valid request data for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
const data = request.data({ date })
expect(data.get('objId')).toBe('1')
})
it('can parse response', () => {
const content =
'[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]'
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-03T21:30:00.000Z',
stop: '2022-03-03T23:04:00.000Z',
title: 'الراقصه و السياسي',
description:
'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .',
icon: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: ''
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=artonline.tv
const { parser, url, request } = require('./artonline.tv.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const channel = {
site_id: 'Aflam2',
xmltv_id: 'ARTAflam2.sa'
}
it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'content-type': 'application/x-www-form-urlencoded'
})
})
it('can generate valid request data for today', () => {
const date = dayjs.utc().startOf('d')
const data = request.data({ date })
expect(data.get('objId')).toBe('0')
})
it('can generate valid request data for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
const data = request.data({ date })
expect(data.get('objId')).toBe('1')
})
it('can parse response', () => {
const content =
'[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]'
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-03-03T21:30:00.000Z',
stop: '2022-03-03T23:04:00.000Z',
title: 'الراقصه و السياسي',
description:
'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .',
icon: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: ''
})
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

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

View file

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

View file

@ -1,130 +1,130 @@
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'beinsports.com',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000, // 1h
interpretHeader: false
}
},
url: function ({ date, channel }) {
let [region] = channel.site_id.split('#')
region = region ? `_${region}` : ''
return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format(
'YYYY-MM-DD'
)}`
},
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel)
let i = 0
items.forEach(item => {
const $item = cheerio.load(item)
const title = parseTitle($item)
if (!title) return
const category = parseCategory($item)
const prev = programs[programs.length - 1]
let start = parseStart($item, date)
if (i === 0 && start.hour() > 18) {
date = date.subtract(1, 'd')
start = start.subtract(1, 'd')
}
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
let stop = parseStop($item, start)
if (stop.isBefore(start)) {
stop = stop.add(1, 'd')
}
programs.push({ title, category, start, stop })
i++
})
return programs
},
async channels({ region, lang }) {
const suffix = region ? `_${region}` : ''
const content = await axios
.get(
`https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08`
)
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(content)
const items = $('.container > div, #epg_div > div').toArray()
return items
.map(item => {
const $item = cheerio.load(item)
const id = $item('*').attr('id')
if (!/^channels_[0-9]+$/.test(id)) return null
const channelId = id.replace('channels_', '')
const imgSrc = $item('img').attr('src')
const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, '']
return {
lang,
site_id: `${region}#${channelId}`,
name
}
})
.filter(i => i)
}
}
function parseTitle($item) {
return $item('.title').text()
}
function parseCategory($item) {
return $item('.format')
.map(function () {
return $item(this).text()
})
.get()
}
function parseStart($item, date) {
let time = $item('.time').text()
if (!time) return null
let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null]
if (!start) return null
start = `${date.format('YYYY-MM-DD')} ${start}${period}`
const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm'
return dayjs.tz(start, format, 'Asia/Qatar')
}
function parseStop($item, date) {
let time = $item('.time').text()
if (!time) return null
let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null]
if (!stop) return null
stop = `${date.format('YYYY-MM-DD')} ${stop}${period}`
const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm'
return dayjs.tz(stop, format, 'Asia/Qatar')
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const $ = cheerio.load(content)
return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray()
}
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'beinsports.com',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000, // 1h
interpretHeader: false
}
},
url: function ({ date, channel }) {
let [region] = channel.site_id.split('#')
region = region ? `_${region}` : ''
return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format(
'YYYY-MM-DD'
)}`
},
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel)
let i = 0
items.forEach(item => {
const $item = cheerio.load(item)
const title = parseTitle($item)
if (!title) return
const category = parseCategory($item)
const prev = programs[programs.length - 1]
let start = parseStart($item, date)
if (i === 0 && start.hour() > 18) {
date = date.subtract(1, 'd')
start = start.subtract(1, 'd')
}
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
let stop = parseStop($item, start)
if (stop.isBefore(start)) {
stop = stop.add(1, 'd')
}
programs.push({ title, category, start, stop })
i++
})
return programs
},
async channels({ region, lang }) {
const suffix = region ? `_${region}` : ''
const content = await axios
.get(
`https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08`
)
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(content)
const items = $('.container > div, #epg_div > div').toArray()
return items
.map(item => {
const $item = cheerio.load(item)
const id = $item('*').attr('id')
if (!/^channels_[0-9]+$/.test(id)) return null
const channelId = id.replace('channels_', '')
const imgSrc = $item('img').attr('src')
const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, '']
return {
lang,
site_id: `${region}#${channelId}`,
name
}
})
.filter(i => i)
}
}
function parseTitle($item) {
return $item('.title').text()
}
function parseCategory($item) {
return $item('.format')
.map(function () {
return $item(this).text()
})
.get()
}
function parseStart($item, date) {
let time = $item('.time').text()
if (!time) return null
let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null]
if (!start) return null
start = `${date.format('YYYY-MM-DD')} ${start}${period}`
const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm'
return dayjs.tz(start, format, 'Asia/Qatar')
}
function parseStop($item, date) {
let time = $item('.time').text()
if (!time) return null
let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null]
if (!stop) return null
stop = `${date.format('YYYY-MM-DD')} ${stop}${period}`
const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm'
return dayjs.tz(stop, format, 'Asia/Qatar')
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const $ = cheerio.load(content)
return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray()
}

View file

@ -1,91 +1,91 @@
// npm run channels:parse -- --config=./sites/beinsports.com/beinsports.com.config.js --output=./sites/beinsports.com/beinsports.com_qa-ar.channels.xml --set=lang:ar --set=region:ar
// npm run grab -- --site=beinsports.com
// npm run grab -- --site=beinsports.com
const { parser, url } = require('./beinsports.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' }
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe(
'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08'
)
})
it('can generate valid url for arabic guide', () => {
const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' }
const result = url({ date, channel })
expect(result).toBe(
'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html'))
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-05-07T19:30:00.000Z',
stop: '2022-05-07T21:20:00.000Z',
title: 'Lorient vs Marseille',
category: ['Ligue 1 2021/22']
})
})
it('can parse response for tomorrow', () => {
const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d')
const content = fs.readFileSync(
path.resolve('sites/beinsports.com/__data__/content_tomorrow.html')
)
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-05-08T21:20:00.000Z',
stop: '2022-05-08T23:10:00.000Z',
title: 'Celtic vs Hearts',
category: ['SPFL Premiership 2021/22']
})
})
it('can parse US response', () => {
const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html'))
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-05-07T20:00:00.000Z',
stop: '2022-05-07T22:00:00.000Z',
title: 'Basaksehir vs. Galatasaray',
category: ['Fútbol Turco Superliga', 'Soccer']
})
})
it('can handle empty guide', () => {
const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html'))
const result = parser({
date,
channel,
content: noContent
})
expect(result).toMatchObject([])
})
// npm run channels:parse -- --config=./sites/beinsports.com/beinsports.com.config.js --output=./sites/beinsports.com/beinsports.com_qa-ar.channels.xml --set=lang:ar --set=region:ar
// npm run grab -- --site=beinsports.com
// npm run grab -- --site=beinsports.com
const { parser, url } = require('./beinsports.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' }
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe(
'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08'
)
})
it('can generate valid url for arabic guide', () => {
const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' }
const result = url({ date, channel })
expect(result).toBe(
'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html'))
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-05-07T19:30:00.000Z',
stop: '2022-05-07T21:20:00.000Z',
title: 'Lorient vs Marseille',
category: ['Ligue 1 2021/22']
})
})
it('can parse response for tomorrow', () => {
const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d')
const content = fs.readFileSync(
path.resolve('sites/beinsports.com/__data__/content_tomorrow.html')
)
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-05-08T21:20:00.000Z',
stop: '2022-05-08T23:10:00.000Z',
title: 'Celtic vs Hearts',
category: ['SPFL Premiership 2021/22']
})
})
it('can parse US response', () => {
const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html'))
const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-05-07T20:00:00.000Z',
stop: '2022-05-07T22:00:00.000Z',
title: 'Basaksehir vs. Galatasaray',
category: ['Fútbol Turco Superliga', 'Soccer']
})
})
it('can handle empty guide', () => {
const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html'))
const result = parser({
date,
channel,
content: noContent
})
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

@ -1,54 +1,54 @@
const dayjs = require('dayjs')
module.exports = {
site: 'bt.com',
days: 2,
url: function ({ date, channel }) {
return `https://voila.metabroadcast.com/4/schedules/${
channel.site_id
}.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=${date.utc().format()}&to=${date
.add(1, 'd')
.utc()
.format()}&source=api.youview.tv&annotations=content.description`
},
parser: function ({ content }) {
const programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: item.item.title,
description: item.item.description,
icon: parseIcon(item),
season: parseSeason(item),
episode: parseEpisode(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseItems(content) {
const data = JSON.parse(content)
return data && data.schedule.entries ? data.schedule.entries : []
}
function parseSeason(item) {
if (item.item.type !== 'episode') return null
return item.item.series_number || null
}
function parseEpisode(item) {
if (item.item.type !== 'episode') return null
return item.item.episode_number || null
}
function parseIcon(item) {
return item.item.image || null
}
function parseStart(item) {
return dayjs(item.broadcast.transmission_time)
}
function parseStop(item) {
return dayjs(item.broadcast.transmission_end_time)
}
const dayjs = require('dayjs')
module.exports = {
site: 'bt.com',
days: 2,
url: function ({ date, channel }) {
return `https://voila.metabroadcast.com/4/schedules/${
channel.site_id
}.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=${date.utc().format()}&to=${date
.add(1, 'd')
.utc()
.format()}&source=api.youview.tv&annotations=content.description`
},
parser: function ({ content }) {
const programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: item.item.title,
description: item.item.description,
icon: parseIcon(item),
season: parseSeason(item),
episode: parseEpisode(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseItems(content) {
const data = JSON.parse(content)
return data && data.schedule.entries ? data.schedule.entries : []
}
function parseSeason(item) {
if (item.item.type !== 'episode') return null
return item.item.series_number || null
}
function parseEpisode(item) {
if (item.item.type !== 'episode') return null
return item.item.episode_number || null
}
function parseIcon(item) {
return item.item.image || null
}
function parseStart(item) {
return dayjs(item.broadcast.transmission_time)
}
function parseStop(item) {
return dayjs(item.broadcast.transmission_end_time)
}

View file

@ -1,52 +1,52 @@
// npm run grab -- --site=bt.com
const { parser, url } = require('./bt.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'hsxv',
xmltv_id: 'BBCOneHD.uk'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://voila.metabroadcast.com/4/schedules/hsxv.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=2022-03-20T00:00:00Z&to=2022-03-21T00:00:00Z&source=api.youview.tv&annotations=content.description'
)
})
it('can parse response', () => {
const content =
'{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[{"broadcast":{"aliases":[{"namespace":"api.youview.tv:slot","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:event-locator","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:pcrid","value":"crid://fp.bbc.co.uk/b/3Q30S2"},{"namespace":"youview:schedule_event:id","value":"79d318f3-b41a-582d-b089-7b0172538b42"}],"transmission_time":"2022-03-19T23:30:00.000Z","transmission_end_time":"2022-03-20T01:20:00.000Z","broadcast_duration":6600,"broadcast_on":"hsxv","schedule_date":null,"repeat":null,"subtitled":true,"signed":null,"audio_described":false,"high_definition":null,"widescreen":null,"surround":null,"live":null,"premiere":null,"continuation":null,"new_series":null,"new_episode":null,"new_one_off":null,"revised_repeat":null,"blackout_restriction":{"all":false}},"item":{"id":"n72nsw","type":"item","display_title":{"title":"The Finest Hours (2016)","subtitle":null},"year":null,"media_type":"video","specialization":"tv","source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"title":"The Finest Hours (2016)","description":"Drama based on a true story, recounting one of history\'s most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.","image":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D","thumbnail":null,"duration":null,"container":null}}]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}'
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'The Finest Hours (2016)',
description:
"Drama based on a true story, recounting one of history's most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.",
icon: 'https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D',
season: null,
episode: null,
start: '2022-03-19T23:30:00.000Z',
stop: '2022-03-20T01:20:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content:
'{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}'
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=bt.com
const { parser, url } = require('./bt.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'hsxv',
xmltv_id: 'BBCOneHD.uk'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://voila.metabroadcast.com/4/schedules/hsxv.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=2022-03-20T00:00:00Z&to=2022-03-21T00:00:00Z&source=api.youview.tv&annotations=content.description'
)
})
it('can parse response', () => {
const content =
'{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[{"broadcast":{"aliases":[{"namespace":"api.youview.tv:slot","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:event-locator","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:pcrid","value":"crid://fp.bbc.co.uk/b/3Q30S2"},{"namespace":"youview:schedule_event:id","value":"79d318f3-b41a-582d-b089-7b0172538b42"}],"transmission_time":"2022-03-19T23:30:00.000Z","transmission_end_time":"2022-03-20T01:20:00.000Z","broadcast_duration":6600,"broadcast_on":"hsxv","schedule_date":null,"repeat":null,"subtitled":true,"signed":null,"audio_described":false,"high_definition":null,"widescreen":null,"surround":null,"live":null,"premiere":null,"continuation":null,"new_series":null,"new_episode":null,"new_one_off":null,"revised_repeat":null,"blackout_restriction":{"all":false}},"item":{"id":"n72nsw","type":"item","display_title":{"title":"The Finest Hours (2016)","subtitle":null},"year":null,"media_type":"video","specialization":"tv","source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"title":"The Finest Hours (2016)","description":"Drama based on a true story, recounting one of history\'s most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.","image":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D","thumbnail":null,"duration":null,"container":null}}]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}'
const result = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'The Finest Hours (2016)',
description:
"Drama based on a true story, recounting one of history's most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.",
icon: 'https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D',
season: null,
episode: null,
start: '2022-03-19T23:30:00.000Z',
stop: '2022-03-20T01:20:00.000Z'
}
])
})
it('can handle empty guide', () => {
const result = parser({
content:
'{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}'
})
expect(result).toMatchObject([])
})

View file

@ -1,109 +1,109 @@
const dayjs = require('dayjs')
const axios = require('axios')
const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'cablego.com.pe',
days: 2,
request: {
method: 'POST',
headers: {
'x-requested-with': 'XMLHttpRequest'
},
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url({ channel, date }) {
const [page] = channel.site_id.split('#')
return `https://cablego.com.pe/epg/default/${date.format(
'YYYY-MM-DD'
)}?page=${page}&do=loadPage`
},
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
const $item = cheerio.load(item)
const prev = programs[programs.length - 1]
let start = parseStart($item, date)
if (!start) return
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: parseTitle($item),
start,
stop
})
})
return programs
},
async channels() {
const promises = [0, 1, 2, 3, 4].map(page => {
return axios.post(
`https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`,
null,
{
headers: {
'x-requested-with': 'XMLHttpRequest'
}
}
)
})
const channels = []
await Promise.allSettled(promises).then(results => {
results.forEach((r, page) => {
if (r.status === 'fulfilled') {
const html = r.value.data.snippets['snippet--channelGrid']
const $ = cheerio.load(html)
$('.epg-channel-strip').each((i, el) => {
const channelId = $(el).find('.epg-channel-logo').attr('id')
channels.push({
lang: 'es',
site_id: `${page}#${channelId}`,
name: $(el).find('img').attr('alt')
})
})
}
})
})
return channels
}
}
function parseTitle($item) {
return $item('span:nth-child(2) > a').text().trim()
}
function parseStart($item, date) {
const time = $item('.epg-show-start').text().trim()
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima')
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return []
const html = data.snippets['snippet--channelGrid']
const $ = cheerio.load(html)
return $(`#${channelId}`).parent().find('.epg-show').toArray()
}
const dayjs = require('dayjs')
const axios = require('axios')
const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'cablego.com.pe',
days: 2,
request: {
method: 'POST',
headers: {
'x-requested-with': 'XMLHttpRequest'
},
cache: {
ttl: 60 * 60 * 1000 // 1 hour
}
},
url({ channel, date }) {
const [page] = channel.site_id.split('#')
return `https://cablego.com.pe/epg/default/${date.format(
'YYYY-MM-DD'
)}?page=${page}&do=loadPage`
},
parser: function ({ content, channel, date }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
const $item = cheerio.load(item)
const prev = programs[programs.length - 1]
let start = parseStart($item, date)
if (!start) return
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({
title: parseTitle($item),
start,
stop
})
})
return programs
},
async channels() {
const promises = [0, 1, 2, 3, 4].map(page => {
return axios.post(
`https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`,
null,
{
headers: {
'x-requested-with': 'XMLHttpRequest'
}
}
)
})
const channels = []
await Promise.allSettled(promises).then(results => {
results.forEach((r, page) => {
if (r.status === 'fulfilled') {
const html = r.value.data.snippets['snippet--channelGrid']
const $ = cheerio.load(html)
$('.epg-channel-strip').each((i, el) => {
const channelId = $(el).find('.epg-channel-logo').attr('id')
channels.push({
lang: 'es',
site_id: `${page}#${channelId}`,
name: $(el).find('img').attr('alt')
})
})
}
})
})
return channels
}
}
function parseTitle($item) {
return $item('span:nth-child(2) > a').text().trim()
}
function parseStart($item, date) {
const time = $item('.epg-show-start').text().trim()
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima')
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return []
const html = data.snippets['snippet--channelGrid']
const $ = cheerio.load(html)
return $(`#${channelId}`).parent().find('.epg-show').toArray()
}

View file

@ -1,54 +1,54 @@
// npm run channels:parse -- --config=./sites/cablego.com.pe/cablego.com.pe.config.js --output=./sites/cablego.com.pe/cablego.com.pe.channels.xml
// npm run grab -- --site=cablego.com.pe
const { parser, url, request } = require('./cablego.com.pe.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0#LATINA',
xmltv_id: 'Latina.pe'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage'
)
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'x-requested-with': 'XMLHttpRequest'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-28T05:00:00.000Z',
stop: '2022-11-28T06:30:00.000Z',
title: 'Especiales Qatar'
})
})
it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const result = parser({ content, channel, date })
expect(result).toMatchObject([])
})
// npm run channels:parse -- --config=./sites/cablego.com.pe/cablego.com.pe.config.js --output=./sites/cablego.com.pe/cablego.com.pe.channels.xml
// npm run grab -- --site=cablego.com.pe
const { parser, url, request } = require('./cablego.com.pe.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '0#LATINA',
xmltv_id: 'Latina.pe'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage'
)
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'x-requested-with': 'XMLHttpRequest'
})
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-28T05:00:00.000Z',
stop: '2022-11-28T06:30:00.000Z',
title: 'Especiales Qatar'
})
})
it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const result = parser({ content, channel, date })
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

@ -1,93 +1,93 @@
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'canalplus-caraibes.com',
days: 2,
url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://service.canal-overseas.com/ott-frontend/vector/53001/channel/${channel.site_id}/events?filter.day=${diff}`
},
async parser({ content }) {
let programs = []
const items = parseItems(content)
for (let item of items) {
if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item)
programs.push({
title: item.title,
description: parseDescription(detail),
category: parseCategory(detail),
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
},
async channels() {
const html = await axios
.get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir')
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(html)
const script = $('body > script:nth-child(2)').html()
const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null]
const data = JSON.parse(json)
const items = data.tvGuide.channels.byZapNumber
return Object.values(items).map(item => {
return {
lang: 'fr',
site_id: item.epgID,
name: item.name
}
})
}
}
async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDescription(detail) {
return detail.detail.informations.summary || null
}
function parseCategory(detail) {
return detail.detail.informations.subGenre || null
}
function parseIcon(item) {
return item.URLImage || item.URLImageDefault
}
function parseStart(item) {
return dayjs.unix(item.startTime)
}
function parseStop(item) {
return dayjs.unix(item.endTime)
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents)
return acc
}, [])
return items
}
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'canalplus-caraibes.com',
days: 2,
url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://service.canal-overseas.com/ott-frontend/vector/53001/channel/${channel.site_id}/events?filter.day=${diff}`
},
async parser({ content }) {
let programs = []
const items = parseItems(content)
for (let item of items) {
if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item)
programs.push({
title: item.title,
description: parseDescription(detail),
category: parseCategory(detail),
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
},
async channels() {
const html = await axios
.get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir')
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(html)
const script = $('body > script:nth-child(2)').html()
const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null]
const data = JSON.parse(json)
const items = data.tvGuide.channels.byZapNumber
return Object.values(items).map(item => {
return {
lang: 'fr',
site_id: item.epgID,
name: item.name
}
})
}
}
async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDescription(detail) {
return detail.detail.informations.summary || null
}
function parseCategory(detail) {
return detail.detail.informations.subGenre || null
}
function parseIcon(item) {
return item.URLImage || item.URLImageDefault
}
function parseStart(item) {
return dayjs.unix(item.startTime)
}
function parseStop(item) {
return dayjs.unix(item.endTime)
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents)
return acc
}, [])
return items
}

View file

@ -1,137 +1,137 @@
// [Geo-blocked] node ./scripts/channels.js --config=./sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js --output=./sites/canalplus-caraibes.com/canalplus-caraibes.com.channels.xml
// npm run grab -- --site=canalplus-caraibes.com
const { parser, url } = require('./canalplus-caraibes.com.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: '50115',
xmltv_id: 'beINSports1France.fr'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1'
)
})
it('can parse response', done => {
const content =
'{"timeSlices":[{"contents":[{"title":"Rugby - Leinster / La Rochelle","subtitle":"Rugby","thirdTitle":"BEIN SPORTS 1 HD","startTime":1660815000,"endTime":1660816800,"onClick":{"displayTemplate":"miniDetail","displayName":"Rugby - Leinster / La Rochelle","URLPage":"https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765","URLVitrine":"https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"},"programID":224515801,"diffusionID":"140377765","URLImageDefault":"https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643","URLImage":"https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771"}],"timeSlice":"4"}]}'
axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') {
return Promise.resolve({
data: JSON.parse(`{
"currentPage": {
"displayName": "Rugby - Leinster / La Rochelle",
"displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"
},
"detail": {
"informations": {
"programmeType": "EPG",
"isInOffer": false,
"isInOfferOnDevice": false,
"isInOfferForD2G": false,
"availableInVoDOnDevice": false,
"availableInVoDOnG5": false,
"availableInD2GOnDevice": false,
"availableInLiveOnDevice": false,
"rediffusions": true,
"canBeRecorded": false,
"channelName": "BEIN SPORTS 1 HD",
"startTime": 1660815000,
"endTime": 1660816800,
"title": "Rugby - Leinster / La Rochelle",
"subtitle": "Rugby",
"thirdTitle": "BEIN SPORTS 1 HD",
"genre": "Sport",
"subGenre": "Rugby",
"editorialTitle": "Sport, France, 0h30",
"audioLanguage": "VF",
"summary": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.",
"summaryMedium": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.",
"programID": 224515801,
"sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html",
"EpgId": 50115,
"CSA": 1,
"HD": false,
"3D": false,
"diffusionID": "140377765",
"duration": "1800",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"
},
"diffusions": [
{
"diffusionDateUTC": 1660815000,
"sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html",
"broadcastId": "140377765",
"name": "BEIN SPORTS 1 HD",
"epgID": "50115",
"ZapNumber": "191",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e"
}
]
}
}`)
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-18T09:30:00.000Z',
stop: '2022-08-18T10:00:00.000Z',
title: 'Rugby - Leinster / La Rochelle',
icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771',
category: 'Rugby',
description:
"Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995."
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content:
'{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})
// [Geo-blocked] node ./scripts/channels.js --config=./sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js --output=./sites/canalplus-caraibes.com/canalplus-caraibes.com.channels.xml
// npm run grab -- --site=canalplus-caraibes.com
const { parser, url } = require('./canalplus-caraibes.com.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: '50115',
xmltv_id: 'beINSports1France.fr'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1'
)
})
it('can parse response', done => {
const content =
'{"timeSlices":[{"contents":[{"title":"Rugby - Leinster / La Rochelle","subtitle":"Rugby","thirdTitle":"BEIN SPORTS 1 HD","startTime":1660815000,"endTime":1660816800,"onClick":{"displayTemplate":"miniDetail","displayName":"Rugby - Leinster / La Rochelle","URLPage":"https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765","URLVitrine":"https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"},"programID":224515801,"diffusionID":"140377765","URLImageDefault":"https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643","URLImage":"https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771"}],"timeSlice":"4"}]}'
axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') {
return Promise.resolve({
data: JSON.parse(`{
"currentPage": {
"displayName": "Rugby - Leinster / La Rochelle",
"displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"
},
"detail": {
"informations": {
"programmeType": "EPG",
"isInOffer": false,
"isInOfferOnDevice": false,
"isInOfferForD2G": false,
"availableInVoDOnDevice": false,
"availableInVoDOnG5": false,
"availableInD2GOnDevice": false,
"availableInLiveOnDevice": false,
"rediffusions": true,
"canBeRecorded": false,
"channelName": "BEIN SPORTS 1 HD",
"startTime": 1660815000,
"endTime": 1660816800,
"title": "Rugby - Leinster / La Rochelle",
"subtitle": "Rugby",
"thirdTitle": "BEIN SPORTS 1 HD",
"genre": "Sport",
"subGenre": "Rugby",
"editorialTitle": "Sport, France, 0h30",
"audioLanguage": "VF",
"summary": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.",
"summaryMedium": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.",
"programID": 224515801,
"sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html",
"EpgId": 50115,
"CSA": 1,
"HD": false,
"3D": false,
"diffusionID": "140377765",
"duration": "1800",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"
},
"diffusions": [
{
"diffusionDateUTC": 1660815000,
"sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html",
"broadcastId": "140377765",
"name": "BEIN SPORTS 1 HD",
"epgID": "50115",
"ZapNumber": "191",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e"
}
]
}
}`)
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-18T09:30:00.000Z',
stop: '2022-08-18T10:00:00.000Z',
title: 'Rugby - Leinster / La Rochelle',
icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771',
category: 'Rugby',
description:
"Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995."
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content:
'{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})

View file

@ -1,94 +1,94 @@
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'canalplus-haiti.com',
days: 2,
url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://service.canal-overseas.com/ott-frontend/vector/53101/channel/${channel.site_id}/events?filter.day=${diff}`
},
async parser({ content }) {
let programs = []
const items = parseItems(content)
for (let item of items) {
if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item)
programs.push({
title: item.title,
description: parseDescription(detail),
category: parseCategory(detail),
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
},
async channels() {
const html = await axios
.get('https://www.canalplus-haiti.com/guide-tv-ce-soir')
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(html)
const script = $('body > script:nth-child(2)').html()
const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null]
const data = JSON.parse(json)
const items = data.tvGuide.channels.byZapNumber
return Object.values(items).map(item => {
return {
lang: 'fr',
site_id: item.epgID,
name: item.name
}
})
}
}
async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDescription(detail) {
return detail.detail.informations.summary || null
}
function parseCategory(detail) {
return detail.detail.informations.subGenre || null
}
function parseIcon(item) {
return item.URLImage || item.URLImageDefault
}
function parseStart(item) {
return dayjs.unix(item.startTime)
}
function parseStop(item) {
return dayjs.unix(item.endTime)
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents)
return acc
}, [])
return items
}
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'canalplus-haiti.com',
days: 2,
url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://service.canal-overseas.com/ott-frontend/vector/53101/channel/${channel.site_id}/events?filter.day=${diff}`
},
async parser({ content }) {
let programs = []
const items = parseItems(content)
for (let item of items) {
if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item)
programs.push({
title: item.title,
description: parseDescription(detail),
category: parseCategory(detail),
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
},
async channels() {
const html = await axios
.get('https://www.canalplus-haiti.com/guide-tv-ce-soir')
.then(r => r.data)
.catch(console.log)
const $ = cheerio.load(html)
const script = $('body > script:nth-child(2)').html()
const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null]
const data = JSON.parse(json)
const items = data.tvGuide.channels.byZapNumber
return Object.values(items).map(item => {
return {
lang: 'fr',
site_id: item.epgID,
name: item.name
}
})
}
}
async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDescription(detail) {
return detail.detail.informations.summary || null
}
function parseCategory(detail) {
return detail.detail.informations.subGenre || null
}
function parseIcon(item) {
return item.URLImage || item.URLImageDefault
}
function parseStart(item) {
return dayjs.unix(item.startTime)
}
function parseStop(item) {
return dayjs.unix(item.endTime)
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents)
return acc
}, [])
return items
}

View file

@ -1,176 +1,176 @@
// [Geo-blocked] npm run channels:parse -- --config=./sites/canalplus-haiti.com/canalplus-haiti.com.config.js --output=./sites/canalplus-haiti.com/canalplus-haiti.com.channels.xml
// npm run grab -- --site=canalplus-haiti.com
const { parser, url } = require('./canalplus-haiti.com.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: '51006',
xmltv_id: 'ViaATV.mq'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1'
)
})
it('can parse response', done => {
const content = `{
"timeSlices": [
{
"contents": [
{
"title": "New Amsterdam - S3 - Ep7",
"subtitle": "Episode 7 - Le mur de la honte",
"thirdTitle": "viaATV",
"startTime": 1660780500,
"endTime": 1660783200,
"onClick": {
"displayTemplate": "miniDetail",
"displayName": "New Amsterdam - S3 - Ep7",
"URLPage": "https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
},
"programID": 187882282,
"diffusionID": "140952809",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e"
}
],
"timeSlice": "2"
}
]
}`
axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') {
return Promise.resolve({
data: JSON.parse(`{
"currentPage": {
"displayName": "New Amsterdam - S3 - Ep7",
"displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
},
"detail": {
"informations": {
"programmeType": "EPG",
"isInOffer": false,
"isInOfferOnDevice": false,
"isInOfferForD2G": false,
"availableInVoDOnDevice": false,
"availableInVoDOnG5": false,
"availableInD2GOnDevice": false,
"availableInLiveOnDevice": false,
"rediffusions": true,
"canBeRecorded": false,
"channelName": "viaATV",
"startTime": 1660780500,
"endTime": 1660783200,
"title": "New Amsterdam - S3 - Ep7",
"subtitle": "Episode 7 - Le mur de la honte",
"thirdTitle": "viaATV",
"genre": "Séries",
"subGenre": "Série Hôpital",
"editorialTitle": "Séries, Etats-Unis, 2020, 0h45",
"audioLanguage": "VF",
"personnalities": [
{
"prefix": "De :",
"content": "Darnell Martin"
},
{
"prefix": "Avec :",
"content": "André De Shields, Anna Suzuki, Anupam Kher, Baylen Thomas, Christine Chang, Craig Wedren, Daniel Dae Kim, Dierdre Friel, Em Grosland, Emma Ramos, Freema Agyeman, Gina Gershon, Graham Norris, Jamie Ann Romero, Janet Montgomery, Jefferson Friedman, Joshua Gitta, Kerry Flanagan, Larry Bryggman, Mike Doyle, Nora Clow, Opal Clow, Ryan Eggold, Simone Policano, Stephen Spinella, Tyler Labine"
}
],
"summary": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.",
"summaryMedium": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.",
"programID": 187882282,
"sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html",
"labels": {
"allocine": false,
"telerama": false,
"sensCritique": false
},
"EpgId": 51006,
"CSA": 1,
"HD": false,
"3D": false,
"diffusionID": "140952809",
"duration": "2700",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
},
"diffusions": [
{
"diffusionDateUTC": 1660780500,
"sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html",
"broadcastId": "140952809",
"name": "viaATV",
"epgID": "51006",
"ZapNumber": "28",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce"
}
]
}
}`)
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-17T23:55:00.000Z',
stop: '2022-08-18T00:40:00.000Z',
title: 'New Amsterdam - S3 - Ep7',
icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e',
category: 'Série Hôpital',
description:
"C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam."
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content:
'{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})
// [Geo-blocked] npm run channels:parse -- --config=./sites/canalplus-haiti.com/canalplus-haiti.com.config.js --output=./sites/canalplus-haiti.com/canalplus-haiti.com.channels.xml
// npm run grab -- --site=canalplus-haiti.com
const { parser, url } = require('./canalplus-haiti.com.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: '51006',
xmltv_id: 'ViaATV.mq'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1'
)
})
it('can parse response', done => {
const content = `{
"timeSlices": [
{
"contents": [
{
"title": "New Amsterdam - S3 - Ep7",
"subtitle": "Episode 7 - Le mur de la honte",
"thirdTitle": "viaATV",
"startTime": 1660780500,
"endTime": 1660783200,
"onClick": {
"displayTemplate": "miniDetail",
"displayName": "New Amsterdam - S3 - Ep7",
"URLPage": "https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
},
"programID": 187882282,
"diffusionID": "140952809",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e"
}
],
"timeSlice": "2"
}
]
}`
axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') {
return Promise.resolve({
data: JSON.parse(`{
"currentPage": {
"displayName": "New Amsterdam - S3 - Ep7",
"displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
},
"detail": {
"informations": {
"programmeType": "EPG",
"isInOffer": false,
"isInOfferOnDevice": false,
"isInOfferForD2G": false,
"availableInVoDOnDevice": false,
"availableInVoDOnG5": false,
"availableInD2GOnDevice": false,
"availableInLiveOnDevice": false,
"rediffusions": true,
"canBeRecorded": false,
"channelName": "viaATV",
"startTime": 1660780500,
"endTime": 1660783200,
"title": "New Amsterdam - S3 - Ep7",
"subtitle": "Episode 7 - Le mur de la honte",
"thirdTitle": "viaATV",
"genre": "Séries",
"subGenre": "Série Hôpital",
"editorialTitle": "Séries, Etats-Unis, 2020, 0h45",
"audioLanguage": "VF",
"personnalities": [
{
"prefix": "De :",
"content": "Darnell Martin"
},
{
"prefix": "Avec :",
"content": "André De Shields, Anna Suzuki, Anupam Kher, Baylen Thomas, Christine Chang, Craig Wedren, Daniel Dae Kim, Dierdre Friel, Em Grosland, Emma Ramos, Freema Agyeman, Gina Gershon, Graham Norris, Jamie Ann Romero, Janet Montgomery, Jefferson Friedman, Joshua Gitta, Kerry Flanagan, Larry Bryggman, Mike Doyle, Nora Clow, Opal Clow, Ryan Eggold, Simone Policano, Stephen Spinella, Tyler Labine"
}
],
"summary": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.",
"summaryMedium": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.",
"programID": 187882282,
"sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html",
"labels": {
"allocine": false,
"telerama": false,
"sensCritique": false
},
"EpgId": 51006,
"CSA": 1,
"HD": false,
"3D": false,
"diffusionID": "140952809",
"duration": "2700",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
},
"diffusions": [
{
"diffusionDateUTC": 1660780500,
"sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html",
"broadcastId": "140952809",
"name": "viaATV",
"epgID": "51006",
"ZapNumber": "28",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce"
}
]
}
}`)
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-17T23:55:00.000Z',
stop: '2022-08-18T00:40:00.000Z',
title: 'New Amsterdam - S3 - Ep7',
icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e',
category: 'Série Hôpital',
description:
"C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam."
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content:
'{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})

View file

@ -1,72 +1,72 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'canalplus-reunion.com',
days: 2,
url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://service.canal-overseas.com/ott-frontend/vector/63001/channel/${channel.site_id}/events?filter.day=${diff}`
},
async parser({ content }) {
let programs = []
const items = parseItems(content)
for (let item of items) {
if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item)
programs.push({
title: item.title,
description: parseDescription(detail),
category: parseCategory(detail),
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
}
}
async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDescription(detail) {
return detail.detail.informations.summary || null
}
function parseCategory(detail) {
return detail.detail.informations.subGenre || null
}
function parseIcon(item) {
return item.URLImage || item.URLImageDefault
}
function parseStart(item) {
return dayjs.unix(item.startTime)
}
function parseStop(item) {
return dayjs.unix(item.endTime)
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents)
return acc
}, [])
return items
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
module.exports = {
site: 'canalplus-reunion.com',
days: 2,
url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://service.canal-overseas.com/ott-frontend/vector/63001/channel/${channel.site_id}/events?filter.day=${diff}`
},
async parser({ content }) {
let programs = []
const items = parseItems(content)
for (let item of items) {
if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item)
programs.push({
title: item.title,
description: parseDescription(detail),
category: parseCategory(detail),
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
}
return programs
}
}
async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseDescription(detail) {
return detail.detail.informations.summary || null
}
function parseCategory(detail) {
return detail.detail.informations.subGenre || null
}
function parseIcon(item) {
return item.URLImage || item.URLImageDefault
}
function parseStart(item) {
return dayjs.unix(item.startTime)
}
function parseStop(item) {
return dayjs.unix(item.endTime)
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents)
return acc
}, [])
return items
}

View file

@ -1,160 +1,160 @@
// npm run grab -- --site=canalplus-reunion.com
const { parser, url } = require('./canalplus-reunion.com.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: '60243',
xmltv_id: 'beINSports2France.fr'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1'
)
})
it('can parse response', done => {
const content = `{
"timeSlices": [
{
"contents": [
{
"title": "Almeria / Real Madrid",
"subtitle": "Football",
"thirdTitle": "BEIN SPORTS 2 HD",
"startTime": 1660780800,
"endTime": 1660788000,
"onClick": {
"displayTemplate": "miniDetail",
"displayName": "Almeria / Real Madrid",
"URLPage": "https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
},
"programID": 224523053,
"diffusionID": "140382363",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20"
}
],
"timeSlice": "4"
}
]
}`
axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') {
return Promise.resolve({
data: JSON.parse(`{
"currentPage": {
"displayName": "Almeria / Real Madrid",
"displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
},
"detail": {
"informations": {
"programmeType": "EPG",
"isInOffer": false,
"isInOfferOnDevice": false,
"isInOfferForD2G": false,
"availableInVoDOnDevice": false,
"availableInVoDOnG5": false,
"availableInD2GOnDevice": false,
"availableInLiveOnDevice": false,
"rediffusions": true,
"canBeRecorded": false,
"channelName": "BEIN SPORTS 2 HD",
"startTime": 1660780800,
"endTime": 1660788000,
"title": "Almeria / Real Madrid",
"subtitle": "Football",
"thirdTitle": "BEIN SPORTS 2 HD",
"genre": "Sport",
"subGenre": "Football",
"editorialTitle": "Sport, Espagne, 2h00",
"audioLanguage": "VF",
"summary": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.",
"summaryMedium": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.",
"programID": 224523053,
"sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html",
"EpgId": 60243,
"CSA": 1,
"HD": false,
"3D": false,
"diffusionID": "140382363",
"duration": "7200",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
},
"diffusions": [
{
"diffusionDateUTC": 1660780800,
"sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html",
"broadcastId": "140382363",
"name": "BEIN SPORTS 2 HD",
"epgID": "60243",
"ZapNumber": "96",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9"
}
]
}
}`)
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-18T00:00:00.000Z',
stop: '2022-08-18T02:00:00.000Z',
title: 'Almeria / Real Madrid',
icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20',
category: 'Football',
description:
"Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date."
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content:
'{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})
// npm run grab -- --site=canalplus-reunion.com
const { parser, url } = require('./canalplus-reunion.com.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const channel = {
site_id: '60243',
xmltv_id: 'beINSports2France.fr'
}
it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0'
)
})
it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1'
)
})
it('can parse response', done => {
const content = `{
"timeSlices": [
{
"contents": [
{
"title": "Almeria / Real Madrid",
"subtitle": "Football",
"thirdTitle": "BEIN SPORTS 2 HD",
"startTime": 1660780800,
"endTime": 1660788000,
"onClick": {
"displayTemplate": "miniDetail",
"displayName": "Almeria / Real Madrid",
"URLPage": "https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
},
"programID": 224523053,
"diffusionID": "140382363",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20"
}
],
"timeSlice": "4"
}
]
}`
axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') {
return Promise.resolve({
data: JSON.parse(`{
"currentPage": {
"displayName": "Almeria / Real Madrid",
"displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
},
"detail": {
"informations": {
"programmeType": "EPG",
"isInOffer": false,
"isInOfferOnDevice": false,
"isInOfferForD2G": false,
"availableInVoDOnDevice": false,
"availableInVoDOnG5": false,
"availableInD2GOnDevice": false,
"availableInLiveOnDevice": false,
"rediffusions": true,
"canBeRecorded": false,
"channelName": "BEIN SPORTS 2 HD",
"startTime": 1660780800,
"endTime": 1660788000,
"title": "Almeria / Real Madrid",
"subtitle": "Football",
"thirdTitle": "BEIN SPORTS 2 HD",
"genre": "Sport",
"subGenre": "Football",
"editorialTitle": "Sport, Espagne, 2h00",
"audioLanguage": "VF",
"summary": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.",
"summaryMedium": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.",
"programID": 224523053,
"sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html",
"EpgId": 60243,
"CSA": 1,
"HD": false,
"3D": false,
"diffusionID": "140382363",
"duration": "7200",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
},
"diffusions": [
{
"diffusionDateUTC": 1660780800,
"sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html",
"broadcastId": "140382363",
"name": "BEIN SPORTS 2 HD",
"epgID": "60243",
"ZapNumber": "96",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9"
}
]
}
}`)
})
} else {
return Promise.resolve({ data: '' })
}
})
parser({ content })
.then(result => {
result = result.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-08-18T00:00:00.000Z',
stop: '2022-08-18T02:00:00.000Z',
title: 'Almeria / Real Madrid',
icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20',
category: 'Football',
description:
"Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date."
}
])
done()
})
.catch(done)
})
it('can handle empty guide', done => {
parser({
content:
'{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(done)
})

View file

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

View file

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

View file

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

View file

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

View file

@ -1,47 +1,47 @@
const dayjs = require('dayjs')
module.exports = {
site: 'chaines-tv.orange.fr',
days: 2,
url({ channel, date }) {
return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date
.add(1, 'd')
.valueOf()}&after=${channel.site_id}&limit=1`
},
parser: function ({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
const start = parseStart(item)
const stop = parseStop(item, start)
programs.push({
title: item.title,
category: item.genreDetailed,
description: item.synopsis,
icon: parseIcon(item),
start: start.toJSON(),
stop: stop.toJSON()
})
})
return programs
}
}
function parseIcon(item) {
return item.covers && item.covers.length ? item.covers[0].url : null
}
function parseStart(item) {
return dayjs.unix(item.diffusionDate)
}
function parseStop(item, start) {
return start.add(item.duration, 's')
}
function parseItems(content, channel) {
const data = JSON.parse(content)
return data && data[channel.site_id] ? data[channel.site_id] : []
}
const dayjs = require('dayjs')
module.exports = {
site: 'chaines-tv.orange.fr',
days: 2,
url({ channel, date }) {
return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date
.add(1, 'd')
.valueOf()}&after=${channel.site_id}&limit=1`
},
parser: function ({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
const start = parseStart(item)
const stop = parseStop(item, start)
programs.push({
title: item.title,
category: item.genreDetailed,
description: item.synopsis,
icon: parseIcon(item),
start: start.toJSON(),
stop: stop.toJSON()
})
})
return programs
}
}
function parseIcon(item) {
return item.covers && item.covers.length ? item.covers[0].url : null
}
function parseStart(item) {
return dayjs.unix(item.diffusionDate)
}
function parseStop(item, start) {
return start.add(item.duration, 's')
}
function parseItems(content, channel) {
const data = JSON.parse(content)
return data && data[channel.site_id] ? data[channel.site_id] : []
}

View file

@ -1,48 +1,48 @@
// npm run grab -- --site=chaines-tv.orange.fr
const { parser, url } = require('./chaines-tv.orange.fr.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '192',
xmltv_id: 'TF1.fr'
}
const content =
'{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"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.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}'
it('can generate valid url', () => {
const result = url({ channel, date })
expect(result).toBe(
'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1'
)
})
it('can parse response', () => {
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste',
description:
"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.",
category: 'Série Suspense',
icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content:
'{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}'
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=chaines-tv.orange.fr
const { parser, url } = require('./chaines-tv.orange.fr.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '192',
xmltv_id: 'TF1.fr'
}
const content =
'{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"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.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}'
it('can generate valid url', () => {
const result = url({ channel, date })
expect(result).toBe(
'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1'
)
})
it('can parse response', () => {
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste',
description:
"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.",
category: 'Série Suspense',
icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content:
'{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}'
})
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

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

View file

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

View file

@ -1,68 +1,68 @@
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'comteco.com.bo',
days: 2,
url: function ({ channel }) {
return `https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/${channel.site_id}`
},
request: {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: function ({ date }) {
const params = new URLSearchParams()
params.append('_method', 'POST')
params.append('fechaini', date.format('D/M/YYYY'))
params.append('fechafin', date.format('D/M/YYYY'))
return params
}
},
parser: function ({ content, date }) {
const programs = []
const items = parseItems(content)
items.forEach(item => {
const prev = programs[programs.length - 1]
const $item = cheerio.load(item)
let start = parseStart($item, date)
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({ title: parseTitle($item), start, stop })
})
return programs
}
}
function parseStart($item, date) {
const timeString = $item('div > div.col-xs-11 > p > span').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'America/La_Paz')
}
function parseTitle($item) {
return $item('div > div.col-xs-11 > p > strong').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#datosasociados > div > .list-group-item').toArray()
}
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'comteco.com.bo',
days: 2,
url: function ({ channel }) {
return `https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/${channel.site_id}`
},
request: {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: function ({ date }) {
const params = new URLSearchParams()
params.append('_method', 'POST')
params.append('fechaini', date.format('D/M/YYYY'))
params.append('fechafin', date.format('D/M/YYYY'))
return params
}
},
parser: function ({ content, date }) {
const programs = []
const items = parseItems(content)
items.forEach(item => {
const prev = programs[programs.length - 1]
const $item = cheerio.load(item)
let start = parseStart($item, date)
if (prev) {
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
date = date.add(1, 'd')
}
prev.stop = start
}
const stop = start.add(30, 'm')
programs.push({ title: parseTitle($item), start, stop })
})
return programs
}
}
function parseStart($item, date) {
const timeString = $item('div > div.col-xs-11 > p > span').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'America/La_Paz')
}
function parseTitle($item) {
return $item('div > div.col-xs-11 > p > strong').text().trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#datosasociados > div > .list-group-item').toArray()
}

View file

@ -1,74 +1,74 @@
// npm run grab -- --site=comteco.com.bo
const { parser, url, request } = require('./comteco.com.bo.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ABYA YALA',
xmltv_id: 'AbyaYalaTV.bo'
}
const content =
'<!DOCTYPE html><html dir="ltr" lang="es"> <head></head> <body class=""> <div id="wrapper" class="clearfix"> <div class="main-content"> <section class="rubroguias"> <div class="container pt-70 pb-40"> <div class="section-content"> <form method="post" accept-charset="utf-8" class="reservation-form mb-0" role="form" id="myform" action="/pages/canales-y-programacion-tv/paquete-oro/ABYA%20YALA" > <div style="display: none"><input type="hidden" name="_method" value="POST"/></div><div class="row"> <div class="col-sm-5"> <div class="col-xs-5 col-sm-7"> <img src="/img/upload/canales/abya-yala.png" alt="" class="img-responsive"/> </div><div class="col-xs-7 col-sm-5 mt-sm-50 mt-lg-50 mt-md-50 mt-xs-20"> <p><strong>Canal Analógico:</strong> 48</p></div></div></div></form> <div class="row"> <div class="col-sm-12"> <div class="row mt-0"> <div class="single-service"> <h3 class=" text-theme-colored line-bottom text-theme-colored mt-0 text-uppercase " > ABYA YALA </h3> <div id="datosasociados"> <div class="list-group"> <div href="#" class="list-group-item bg-white-f1"> <div class="row"> <div class="col-xs-11"> <p class="mb-0"> <span class="text-red mr-15">00:00:00</span> <strong>Abya Yala noticias - 3ra edición</strong> </p></div></div></div><div href="#" class="list-group-item bg-white-f1"> <div class="row"> <div class="col-xs-11"> <p class="mb-0"> <span class="text-red mr-15">01:00:00</span> <strong>Cierre de emisión</strong> </p></div></div></div><div href="#" class="list-group-item bg-white-f1"> <div class="row"> <div class="col-xs-11"> <p class="mb-0"> <span class="text-red mr-15">23:00:00</span> <strong>Referentes</strong> </p></div></div></div><p class="mt-20"> <a href="/pages/canales-y-programacion-tv" class="btn btn-border btn-gray btn-transparent btn-circled" >Regresar a canales</a > </p></div></div></div></div></div></div></div></div></section> </div></div></body></html>'
it('can generate valid url', () => {
expect(url({ channel })).toBe(
'https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/ABYA YALA'
)
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/x-www-form-urlencoded'
})
})
it('can generate valid request data', () => {
const result = request.data({ date })
expect(result.get('_method')).toBe('POST')
expect(result.get('fechaini')).toBe('25/11/2021')
expect(result.get('fechafin')).toBe('25/11/2021')
})
it('can parse response', () => {
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-25T04:00:00.000Z',
stop: '2021-11-25T05:00:00.000Z',
title: 'Abya Yala noticias - 3ra edición'
},
{
start: '2021-11-25T05:00:00.000Z',
stop: '2021-11-26T03:00:00.000Z',
title: 'Cierre de emisión'
},
{
start: '2021-11-26T03:00:00.000Z',
stop: '2021-11-26T03:30:00.000Z',
title: 'Referentes'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=comteco.com.bo
const { parser, url, request } = require('./comteco.com.bo.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'ABYA YALA',
xmltv_id: 'AbyaYalaTV.bo'
}
const content =
'<!DOCTYPE html><html dir="ltr" lang="es"> <head></head> <body class=""> <div id="wrapper" class="clearfix"> <div class="main-content"> <section class="rubroguias"> <div class="container pt-70 pb-40"> <div class="section-content"> <form method="post" accept-charset="utf-8" class="reservation-form mb-0" role="form" id="myform" action="/pages/canales-y-programacion-tv/paquete-oro/ABYA%20YALA" > <div style="display: none"><input type="hidden" name="_method" value="POST"/></div><div class="row"> <div class="col-sm-5"> <div class="col-xs-5 col-sm-7"> <img src="/img/upload/canales/abya-yala.png" alt="" class="img-responsive"/> </div><div class="col-xs-7 col-sm-5 mt-sm-50 mt-lg-50 mt-md-50 mt-xs-20"> <p><strong>Canal Analógico:</strong> 48</p></div></div></div></form> <div class="row"> <div class="col-sm-12"> <div class="row mt-0"> <div class="single-service"> <h3 class=" text-theme-colored line-bottom text-theme-colored mt-0 text-uppercase " > ABYA YALA </h3> <div id="datosasociados"> <div class="list-group"> <div href="#" class="list-group-item bg-white-f1"> <div class="row"> <div class="col-xs-11"> <p class="mb-0"> <span class="text-red mr-15">00:00:00</span> <strong>Abya Yala noticias - 3ra edición</strong> </p></div></div></div><div href="#" class="list-group-item bg-white-f1"> <div class="row"> <div class="col-xs-11"> <p class="mb-0"> <span class="text-red mr-15">01:00:00</span> <strong>Cierre de emisión</strong> </p></div></div></div><div href="#" class="list-group-item bg-white-f1"> <div class="row"> <div class="col-xs-11"> <p class="mb-0"> <span class="text-red mr-15">23:00:00</span> <strong>Referentes</strong> </p></div></div></div><p class="mt-20"> <a href="/pages/canales-y-programacion-tv" class="btn btn-border btn-gray btn-transparent btn-circled" >Regresar a canales</a > </p></div></div></div></div></div></div></div></div></section> </div></div></body></html>'
it('can generate valid url', () => {
expect(url({ channel })).toBe(
'https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/ABYA YALA'
)
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/x-www-form-urlencoded'
})
})
it('can generate valid request data', () => {
const result = request.data({ date })
expect(result.get('_method')).toBe('POST')
expect(result.get('fechaini')).toBe('25/11/2021')
expect(result.get('fechafin')).toBe('25/11/2021')
})
it('can parse response', () => {
const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2021-11-25T04:00:00.000Z',
stop: '2021-11-25T05:00:00.000Z',
title: 'Abya Yala noticias - 3ra edición'
},
{
start: '2021-11-25T05:00:00.000Z',
stop: '2021-11-26T03:00:00.000Z',
title: 'Cierre de emisión'
},
{
start: '2021-11-26T03:00:00.000Z',
stop: '2021-11-26T03:30:00.000Z',
title: 'Referentes'
}
])
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})

View file

@ -1,78 +1,78 @@
const cheerio = require('cheerio')
const { DateTime } = require('luxon')
module.exports = {
site: 'cosmote.gr',
days: 2,
url: function ({ date, channel }) {
return `https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=${date.format(
'DD-MM-YYYY'
)}&_channelprogram_WAR_OTETVportlet_articleTitleUrl=${channel.site_id}`
},
parser: function ({ date, content }) {
let programs = []
const items = parseItems(content)
items.forEach((item, i) => {
const prev = programs[programs.length - 1]
const $item = cheerio.load(item)
let start = parseStart($item, date)
if (i === 0 && start.hour > 12 && start.hour < 21) {
date = date.subtract(1, 'd')
start = start.minus({ days: 1 })
}
if (prev && start < prev.start) {
start = start.plus({ days: 1 })
date = date.add(1, 'd')
}
let stop = parseStop($item, date)
if (stop < start) {
stop = stop.plus({ days: 1 })
date = date.add(1, 'd')
}
programs.push({
title: parseTitle($item),
category: parseCategory($item),
start,
stop
})
})
return programs
}
}
function parseTitle($item) {
return $item('.channel_program-table--program > a').text()
}
function parseCategory($item) {
const typeString = $item('.channel_program-table--program_type')
.children()
.remove()
.end()
.text()
.trim()
const [, category] = typeString.match(/\| (.*)/) || [null, null]
return category
}
function parseStart($item, date) {
const timeString = $item('span.start-time').text()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC()
}
function parseStop($item, date) {
const timeString = $item('span.end-time').text()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#_channelprogram_WAR_OTETVportlet_programs > tr.d-sm-table-row').toArray()
}
const cheerio = require('cheerio')
const { DateTime } = require('luxon')
module.exports = {
site: 'cosmote.gr',
days: 2,
url: function ({ date, channel }) {
return `https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=${date.format(
'DD-MM-YYYY'
)}&_channelprogram_WAR_OTETVportlet_articleTitleUrl=${channel.site_id}`
},
parser: function ({ date, content }) {
let programs = []
const items = parseItems(content)
items.forEach((item, i) => {
const prev = programs[programs.length - 1]
const $item = cheerio.load(item)
let start = parseStart($item, date)
if (i === 0 && start.hour > 12 && start.hour < 21) {
date = date.subtract(1, 'd')
start = start.minus({ days: 1 })
}
if (prev && start < prev.start) {
start = start.plus({ days: 1 })
date = date.add(1, 'd')
}
let stop = parseStop($item, date)
if (stop < start) {
stop = stop.plus({ days: 1 })
date = date.add(1, 'd')
}
programs.push({
title: parseTitle($item),
category: parseCategory($item),
start,
stop
})
})
return programs
}
}
function parseTitle($item) {
return $item('.channel_program-table--program > a').text()
}
function parseCategory($item) {
const typeString = $item('.channel_program-table--program_type')
.children()
.remove()
.end()
.text()
.trim()
const [, category] = typeString.match(/\| (.*)/) || [null, null]
return category
}
function parseStart($item, date) {
const timeString = $item('span.start-time').text()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC()
}
function parseStop($item, date) {
const timeString = $item('span.end-time').text()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('#_channelprogram_WAR_OTETVportlet_programs > tr.d-sm-table-row').toArray()
}

View file

@ -1,79 +1,79 @@
// npm run grab -- --site=cosmote.gr
const { parser, url } = require('./cosmote.gr.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const date = dayjs.utc('2023-06-08', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '4e',
xmltv_id: '4E.gr'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=08-06-2023&_channelprogram_WAR_OTETVportlet_articleTitleUrl=4e'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content1.html'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-07T20:30:00.000Z',
stop: '2023-06-07T21:45:00.000Z',
title: 'Τηλεφημερίδα',
category: 'Εκπομπή - Μαγκαζίνο'
})
expect(results[30]).toMatchObject({
start: '2023-06-08T19:45:00.000Z',
stop: '2023-06-08T20:30:00.000Z',
title: 'Μικρό Απόδειπνο',
category: 'Special'
})
})
it('can parse response when the guide starting before midnight', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content2.html'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-07T21:30:00.000Z',
stop: '2023-06-07T22:30:00.000Z',
title: 'Καλύτερα Αργά',
category: 'Ψυχαγωγική Εκπομπή'
})
expect(results[22]).toMatchObject({
start: '2023-06-08T19:00:00.000Z',
stop: '2023-06-08T21:30:00.000Z',
title: 'Πίσω Από Τις Γραμμές',
category: 'Εκπομπή - Μαγκαζίνο'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=cosmote.gr
const { parser, url } = require('./cosmote.gr.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const date = dayjs.utc('2023-06-08', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '4e',
xmltv_id: '4E.gr'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=08-06-2023&_channelprogram_WAR_OTETVportlet_articleTitleUrl=4e'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content1.html'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-07T20:30:00.000Z',
stop: '2023-06-07T21:45:00.000Z',
title: 'Τηλεφημερίδα',
category: 'Εκπομπή - Μαγκαζίνο'
})
expect(results[30]).toMatchObject({
start: '2023-06-08T19:45:00.000Z',
stop: '2023-06-08T20:30:00.000Z',
title: 'Μικρό Απόδειπνο',
category: 'Special'
})
})
it('can parse response when the guide starting before midnight', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content2.html'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-06-07T21:30:00.000Z',
stop: '2023-06-07T22:30:00.000Z',
title: 'Καλύτερα Αργά',
category: 'Ψυχαγωγική Εκπομπή'
})
expect(results[22]).toMatchObject({
start: '2023-06-08T19:00:00.000Z',
stop: '2023-06-08T21:30:00.000Z',
title: 'Πίσω Από Τις Γραμμές',
category: 'Εκπομπή - Μαγκαζίνο'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
})
expect(result).toMatchObject([])
})

View file

@ -1,70 +1,70 @@
const axios = require('axios')
const dayjs = require('dayjs')
module.exports = {
site: 'delta.nl',
days: 2,
url: function ({ channel, date }) {
return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date
.add(1, 'd')
.unix()}&includeDetails=true&channels=${channel.site_id}`
},
async parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
for (let item of items) {
const details = await loadProgramDetails(item)
programs.push({
title: item.title,
icon: item.images.thumbnail.url,
description: details.description,
start: parseStart(item).toJSON(),
stop: parseStop(item).toJSON()
})
}
return programs
},
async channels() {
const items = await axios
.get('https://clientapi.tv.delta.nl/channels/list')
.then(r => r.data)
.catch(console.log)
return items
.filter(i => i.type === 'TV')
.map(item => {
return {
lang: 'nl',
site_id: item['ID'],
name: item.name
}
})
}
}
async function loadProgramDetails(item) {
if (!item.ID) return {}
const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5`
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseStart(item) {
return dayjs.unix(item.start)
}
function parseStop(item) {
return dayjs.unix(item.end)
}
function parseItems(content, channel) {
const data = JSON.parse(content)
if (!data) return []
return data[channel.site_id] || []
}
const axios = require('axios')
const dayjs = require('dayjs')
module.exports = {
site: 'delta.nl',
days: 2,
url: function ({ channel, date }) {
return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date
.add(1, 'd')
.unix()}&includeDetails=true&channels=${channel.site_id}`
},
async parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
for (let item of items) {
const details = await loadProgramDetails(item)
programs.push({
title: item.title,
icon: item.images.thumbnail.url,
description: details.description,
start: parseStart(item).toJSON(),
stop: parseStop(item).toJSON()
})
}
return programs
},
async channels() {
const items = await axios
.get('https://clientapi.tv.delta.nl/channels/list')
.then(r => r.data)
.catch(console.log)
return items
.filter(i => i.type === 'TV')
.map(item => {
return {
lang: 'nl',
site_id: item['ID'],
name: item.name
}
})
}
}
async function loadProgramDetails(item) {
if (!item.ID) return {}
const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5`
const data = await axios
.get(url)
.then(r => r.data)
.catch(console.log)
return data || {}
}
function parseStart(item) {
return dayjs.unix(item.start)
}
function parseStop(item) {
return dayjs.unix(item.end)
}
function parseItems(content, channel) {
const data = JSON.parse(content)
if (!data) return []
return data[channel.site_id] || []
}

View file

@ -1,70 +1,70 @@
// npm run channels:parse -- --config=./sites/delta.nl/delta.nl.config.js --output=./sites/delta.nl/delta.nl.channels.xml
// npm run grab -- --site=delta.nl
const { parser, url } = require('./delta.nl.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1',
xmltv_id: 'NPO1.nl'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1'
)
})
it('can parse response', done => {
axios.get.mockImplementation(() =>
Promise.resolve({
data: JSON.parse(
'{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}'
)
})
)
const content =
'{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}'
parser({ date, channel, content })
.then(result => {
expect(result).toMatchObject([
{
start: '2021-11-11T23:56:00.000Z',
stop: '2021-11-12T00:22:00.000Z',
title: 'NOS Journaal',
description:
'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.',
icon: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg'
}
])
done()
})
.catch(error => {
done(error)
})
})
it('can handle empty guide', done => {
parser({
date,
channel,
content: '{"code":500,"message":"Error retrieving guide"}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(error => {
done(error)
})
})
// npm run channels:parse -- --config=./sites/delta.nl/delta.nl.config.js --output=./sites/delta.nl/delta.nl.channels.xml
// npm run grab -- --site=delta.nl
const { parser, url } = require('./delta.nl.config.js')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1',
xmltv_id: 'NPO1.nl'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1'
)
})
it('can parse response', done => {
axios.get.mockImplementation(() =>
Promise.resolve({
data: JSON.parse(
'{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}'
)
})
)
const content =
'{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}'
parser({ date, channel, content })
.then(result => {
expect(result).toMatchObject([
{
start: '2021-11-11T23:56:00.000Z',
stop: '2021-11-12T00:22:00.000Z',
title: 'NOS Journaal',
description:
'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.',
icon: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg'
}
])
done()
})
.catch(error => {
done(error)
})
})
it('can handle empty guide', done => {
parser({
date,
channel,
content: '{"code":500,"message":"Error retrieving guide"}'
})
.then(result => {
expect(result).toMatchObject([])
done()
})
.catch(error => {
done(error)
})
})

View file

@ -1,77 +1,77 @@
const _ = require('lodash')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(timezone)
// category list is not complete
// const categories = {
// '00': 'Diğer',
// E0: 'Romantik Komedi',
// E1: 'Aksiyon',
// E4: 'Macera',
// E5: 'Dram',
// E6: 'Fantastik',
// E7: 'Komedi',
// E8: 'Korku',
// EB: 'Polisiye',
// EF: 'Western',
// FA: 'Macera',
// FB: 'Yarışma',
// FC: 'Eğlence',
// F0: 'Reality-Show',
// F2: 'Haberler',
// F4: 'Belgesel',
// F6: 'Eğitim',
// F7: 'Sanat ve Kültür',
// F9: 'Life Style'
// }
module.exports = {
site: 'digiturk.com.tr',
days: 2,
url: function ({ date, channel }) {
return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${
channel.site_id
}&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false`
},
request: {
method: 'GET',
headers: {
Referer: 'https://www.digiturk.com.tr/'
}
},
parser: function ({ content }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: item.PName,
// description: item.LongDescription,
// category: parseCategory(item),
start: parseTime(item.PStartTime),
stop: parseTime(item.PEndTime)
})
})
programs = _.sortBy(programs, 'start')
return programs
}
}
function parseTime(time) {
let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', ''))
return dayjs(timestamp)
}
// function parseCategory(item) {
// return (item.PGenre) ? categories[item.PGenre] : null
// }
function parseItems(content) {
if (!content) return []
const data = JSON.parse(content)
return data && data.BChannels && data.BChannels[0].CPrograms ? data.BChannels[0].CPrograms : []
}
const _ = require('lodash')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(timezone)
// category list is not complete
// const categories = {
// '00': 'Diğer',
// E0: 'Romantik Komedi',
// E1: 'Aksiyon',
// E4: 'Macera',
// E5: 'Dram',
// E6: 'Fantastik',
// E7: 'Komedi',
// E8: 'Korku',
// EB: 'Polisiye',
// EF: 'Western',
// FA: 'Macera',
// FB: 'Yarışma',
// FC: 'Eğlence',
// F0: 'Reality-Show',
// F2: 'Haberler',
// F4: 'Belgesel',
// F6: 'Eğitim',
// F7: 'Sanat ve Kültür',
// F9: 'Life Style'
// }
module.exports = {
site: 'digiturk.com.tr',
days: 2,
url: function ({ date, channel }) {
return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${
channel.site_id
}&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false`
},
request: {
method: 'GET',
headers: {
Referer: 'https://www.digiturk.com.tr/'
}
},
parser: function ({ content }) {
let programs = []
const items = parseItems(content)
items.forEach(item => {
programs.push({
title: item.PName,
// description: item.LongDescription,
// category: parseCategory(item),
start: parseTime(item.PStartTime),
stop: parseTime(item.PEndTime)
})
})
programs = _.sortBy(programs, 'start')
return programs
}
}
function parseTime(time) {
let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', ''))
return dayjs(timestamp)
}
// function parseCategory(item) {
// return (item.PGenre) ? categories[item.PGenre] : null
// }
function parseItems(content) {
if (!content) return []
const data = JSON.parse(content)
return data && data.BChannels && data.BChannels[0].CPrograms ? data.BChannels[0].CPrograms : []
}

View file

@ -1,49 +1,49 @@
// npm run grab -- --site=digiturk.com.tr
const { parser, url } = require('./digiturk.com.tr.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '14',
xmltv_id: 'beINMovies2Action.qa'
}
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe(
'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-01-18T20:40:00.000Z',
stop: '2023-01-18T22:32:00.000Z',
title: 'PARÇALANMIŞ'
})
expect(results[10]).toMatchObject({
start: '2023-01-19T05:04:00.000Z',
stop: '2023-01-19T06:42:00.000Z',
title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI'
})
})
it('can handle empty guide', () => {
const result = parser({ content: '' })
expect(result).toMatchObject([])
})
// npm run grab -- --site=digiturk.com.tr
const { parser, url } = require('./digiturk.com.tr.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '14',
xmltv_id: 'beINMovies2Action.qa'
}
it('can generate valid url', () => {
const result = url({ date, channel })
expect(result).toBe(
'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-01-18T20:40:00.000Z',
stop: '2023-01-18T22:32:00.000Z',
title: 'PARÇALANMIŞ'
})
expect(results[10]).toMatchObject({
start: '2023-01-19T05:04:00.000Z',
stop: '2023-01-19T06:42:00.000Z',
title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI'
})
})
it('can handle empty guide', () => {
const result = parser({ content: '' })
expect(result).toMatchObject([])
})

View file

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

View file

@ -1,79 +1,79 @@
// npm run grab -- --site=directv.com.ar
const { parser, url, request } = require('./directv.com.ar.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-06-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '207#A&amp;EHD',
xmltv_id: 'AEHDSouth.us'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;'
})
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
filterParameters: {
day: 19,
time: 0,
minute: 0,
month: 6,
year: 2022,
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum: '207',
channelName: 'A&EHD'
}
})
})
it('can parse response', () => {
const content =
'{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}'
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-06-19T03:00:00.000Z',
stop: '2022-06-19T03:15:00.000Z',
title: 'Chicas guapas',
description:
'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.',
rating: {
system: 'MPA',
value: 'NR'
}
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '',
channel
})
expect(result).toMatchObject([])
})
// npm run grab -- --site=directv.com.ar
const { parser, url, request } = require('./directv.com.ar.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-06-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '207#A&amp;EHD',
xmltv_id: 'AEHDSouth.us'
}
it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming')
})
it('can generate valid request method', () => {
expect(request.method).toBe('POST')
})
it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({
'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;'
})
})
it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({
filterParameters: {
day: 19,
time: 0,
minute: 0,
month: 6,
year: 2022,
offSetValue: 0,
filtersScreenFilters: [''],
isHd: '',
isChannelDetails: 'Y',
channelNum: '207',
channelName: 'A&EHD'
}
})
})
it('can parse response', () => {
const content =
'{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}'
const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2022-06-19T03:00:00.000Z',
stop: '2022-06-19T03:15:00.000Z',
title: 'Chicas guapas',
description:
'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.',
rating: {
system: 'MPA',
value: 'NR'
}
}
])
})
it('can handle empty guide', () => {
const result = parser({
content: '',
channel
})
expect(result).toMatchObject([])
})

View file

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

View file

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

View file

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

View file

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

View file

@ -1,145 +1,145 @@
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'dishtv.in',
days: 2,
url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram',
request: {
method: 'POST',
data({ channel, date }) {
return {
Channelarr: channel.site_id,
fromdate: date.format('YYYYMMDDHHmm'),
todate: date.add(1, 'd').format('YYYYMMDDHHmm')
}
}
},
parser: function ({ content, date }) {
let programs = []
const data = parseContent(content)
const items = parseItems(data)
items.forEach(item => {
const title = parseTitle(item)
const start = parseStart(item, date)
const stop = parseStop(item, start)
if (title === 'No Information Available') return
programs.push({
title,
start: start.toString(),
stop: stop.toString()
})
})
return programs
},
async channels() {
const channelguide = await axios
.get('https://www.dishtv.in/channelguide/')
.then(r => r.data)
.catch(console.log)
const $channelguide = cheerio.load(channelguide)
let ids = []
$channelguide('#MainContent_recordPagging li').each((i, item) => {
const onclick = $channelguide(item).find('a').attr('onclick')
const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null]
ids = ids.concat(list.split(','))
})
ids = ids.filter(Boolean)
const channels = {}
const channelList = await axios
.post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', {
strChannel: ''
})
.then(r => r.data)
.catch(console.log)
const $channelList = cheerio.load(channelList.d)
$channelList('#tblpackChnl > div').each((i, item) => {
let num = $channelList(item).find('p:nth-child(2)').text().trim()
const name = $channelList(item).find('p').first().text().trim()
if (num === '') return
channels[parseInt(num)] = {
name
}
})
const date = dayjs().add(1, 'd')
const promises = []
for (let id of ids) {
const promise = axios
.post(
'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram',
{
Channelarr: id,
fromdate: date.format('YYYYMMDD[0000]'),
todate: date.format('YYYYMMDD[2300]')
},
{ timeout: 5000 }
)
.then(r => r.data)
.then(data => {
const $channelGuide = cheerio.load(data.d)
const num = $channelGuide('.cnl-fav > a > span').text().trim()
if (channels[num]) {
channels[num].site_id = id
}
})
.catch(console.log)
promises.push(promise)
}
await Promise.allSettled(promises)
return Object.values(channels)
}
}
function parseTitle(item) {
const $ = cheerio.load(item)
return $('a').text()
}
function parseStart(item) {
const $ = cheerio.load(item)
const onclick = $('i.fa-circle').attr('onclick')
const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/)
return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata')
}
function parseStop(item, start) {
const $ = cheerio.load(item)
const duration = $('*').data('time')
return start.add(duration, 'm')
}
function parseContent(content) {
const data = JSON.parse(content)
return data.d
}
function parseItems(data) {
const $ = cheerio.load(data)
return $('.datatime').toArray()
}
const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'dishtv.in',
days: 2,
url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram',
request: {
method: 'POST',
data({ channel, date }) {
return {
Channelarr: channel.site_id,
fromdate: date.format('YYYYMMDDHHmm'),
todate: date.add(1, 'd').format('YYYYMMDDHHmm')
}
}
},
parser: function ({ content, date }) {
let programs = []
const data = parseContent(content)
const items = parseItems(data)
items.forEach(item => {
const title = parseTitle(item)
const start = parseStart(item, date)
const stop = parseStop(item, start)
if (title === 'No Information Available') return
programs.push({
title,
start: start.toString(),
stop: stop.toString()
})
})
return programs
},
async channels() {
const channelguide = await axios
.get('https://www.dishtv.in/channelguide/')
.then(r => r.data)
.catch(console.log)
const $channelguide = cheerio.load(channelguide)
let ids = []
$channelguide('#MainContent_recordPagging li').each((i, item) => {
const onclick = $channelguide(item).find('a').attr('onclick')
const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null]
ids = ids.concat(list.split(','))
})
ids = ids.filter(Boolean)
const channels = {}
const channelList = await axios
.post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', {
strChannel: ''
})
.then(r => r.data)
.catch(console.log)
const $channelList = cheerio.load(channelList.d)
$channelList('#tblpackChnl > div').each((i, item) => {
let num = $channelList(item).find('p:nth-child(2)').text().trim()
const name = $channelList(item).find('p').first().text().trim()
if (num === '') return
channels[parseInt(num)] = {
name
}
})
const date = dayjs().add(1, 'd')
const promises = []
for (let id of ids) {
const promise = axios
.post(
'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram',
{
Channelarr: id,
fromdate: date.format('YYYYMMDD[0000]'),
todate: date.format('YYYYMMDD[2300]')
},
{ timeout: 5000 }
)
.then(r => r.data)
.then(data => {
const $channelGuide = cheerio.load(data.d)
const num = $channelGuide('.cnl-fav > a > span').text().trim()
if (channels[num]) {
channels[num].site_id = id
}
})
.catch(console.log)
promises.push(promise)
}
await Promise.allSettled(promises)
return Object.values(channels)
}
}
function parseTitle(item) {
const $ = cheerio.load(item)
return $('a').text()
}
function parseStart(item) {
const $ = cheerio.load(item)
const onclick = $('i.fa-circle').attr('onclick')
const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/)
return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata')
}
function parseStop(item, start) {
const $ = cheerio.load(item)
const duration = $('*').data('time')
return start.add(duration, 'm')
}
function parseContent(content) {
const data = JSON.parse(content)
return data.d
}
function parseItems(data) {
const $ = cheerio.load(data)
return $('.datatime').toArray()
}

View file

@ -1,45 +1,45 @@
// npm run channels:parse -- --config=./sites/dishtv.in/dishtv.in.config.js --output=./sites/dishtv.in/dishtv.in.channels.xml
// npm run grab -- --site=dishtv.in
const { parser, url, request } = require('./dishtv.in.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' }
const content =
'{"d":"\\u003cdiv class=\\"pgrid\\"\\u003e\\u003cdiv class=\\"img sm-30 grid\\"\\u003e\\u003cimg class=\\"chnl-logo\\" src=\\"http://imagesdishtvd2h.whatsonindia.com/dasimages/channel/landscape/360x270/hiyj8ndf.png\\" onclick=\\"ShowChannelGuid(\\u0027womens-active\\u0027,\\u002710000000075992337\\u0027);\\" /\\u003e\\u003cdiv class=\\"cnl-fav\\"\\u003e\\u003ca href=\\"javascript:;\\"\\u003e\\u003cem\\u003ech. no\\u003c/em\\u003e\\u003cspan\\u003e117\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/div\\u003e\\u003ci class=\\"fa fa-heart Set_Favourite_Channel\\" aria-hidden=\\"true\\" title=\\"Set womens active channel as your favourite channel\\" onclick=\\"SetFavouriteChannel();\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003cdiv class=\\"grid-wrap\\"\\u003e\\u003cdiv class=\\"sm-30 grid datatime\\" data-time=\\"24\\" data-starttime=\\"12:00 AM\\" data-endttime=\\"12:24 AM\\" data-reamintime=\\"0\\"\\u003e\\u003ca title=\\"Event Name: Cynthia Williams - Diwali Look Part 01\\r\\nStart Time: 12:00 AM\\r\\nDuration: 24min\\r\\nSynopsis: Learn diwali look by cynthia williams p1\\r\\n\\" href=\\"javascript:;\\" onclick=\\"ShowCurrentTime(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111051200\\u0027)\\"\\u003eCynthia Williams - Diwali Look Part 01\\u003c/a\\u003e\\u003cdiv class=\\"cnlSerialIcon\\"\\u003e\\u003ci class=\\"fa fa-heart\\" aria-hidden=\\"true\\" title=\\"Set Favourite Serial\\" onclick=\\"SetFavouriteShow();\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-clock-o\\" aria-hidden=\\"true\\" title=\\"Reminder Serial\\" onclick=\\"ReminderEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027)\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-circle\\" aria-hidden=\\"true\\" title=\\"Record Serial\\" onclick=\\"RecordingEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027,30000000550913679)\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e"}'
it('can generate valid url', () => {
expect(url).toBe(
'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram'
)
})
it('can generate valid request data', () => {
const result = request.data({ channel, date })
expect(result).toMatchObject({
Channelarr: '10000000075992337',
fromdate: '202111050000',
todate: '202111060000'
})
})
it('can parse response', () => {
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: 'Thu, 04 Nov 2021 18:30:00 GMT',
stop: 'Thu, 04 Nov 2021 18:54:00 GMT',
title: 'Cynthia Williams - Diwali Look Part 01'
}
])
})
it('can handle empty guide', () => {
const result = parser({ date, channel, content: '{"d":""}' })
expect(result).toMatchObject([])
})
// npm run channels:parse -- --config=./sites/dishtv.in/dishtv.in.config.js --output=./sites/dishtv.in/dishtv.in.channels.xml
// npm run grab -- --site=dishtv.in
const { parser, url, request } = require('./dishtv.in.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' }
const content =
'{"d":"\\u003cdiv class=\\"pgrid\\"\\u003e\\u003cdiv class=\\"img sm-30 grid\\"\\u003e\\u003cimg class=\\"chnl-logo\\" src=\\"http://imagesdishtvd2h.whatsonindia.com/dasimages/channel/landscape/360x270/hiyj8ndf.png\\" onclick=\\"ShowChannelGuid(\\u0027womens-active\\u0027,\\u002710000000075992337\\u0027);\\" /\\u003e\\u003cdiv class=\\"cnl-fav\\"\\u003e\\u003ca href=\\"javascript:;\\"\\u003e\\u003cem\\u003ech. no\\u003c/em\\u003e\\u003cspan\\u003e117\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/div\\u003e\\u003ci class=\\"fa fa-heart Set_Favourite_Channel\\" aria-hidden=\\"true\\" title=\\"Set womens active channel as your favourite channel\\" onclick=\\"SetFavouriteChannel();\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003cdiv class=\\"grid-wrap\\"\\u003e\\u003cdiv class=\\"sm-30 grid datatime\\" data-time=\\"24\\" data-starttime=\\"12:00 AM\\" data-endttime=\\"12:24 AM\\" data-reamintime=\\"0\\"\\u003e\\u003ca title=\\"Event Name: Cynthia Williams - Diwali Look Part 01\\r\\nStart Time: 12:00 AM\\r\\nDuration: 24min\\r\\nSynopsis: Learn diwali look by cynthia williams p1\\r\\n\\" href=\\"javascript:;\\" onclick=\\"ShowCurrentTime(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111051200\\u0027)\\"\\u003eCynthia Williams - Diwali Look Part 01\\u003c/a\\u003e\\u003cdiv class=\\"cnlSerialIcon\\"\\u003e\\u003ci class=\\"fa fa-heart\\" aria-hidden=\\"true\\" title=\\"Set Favourite Serial\\" onclick=\\"SetFavouriteShow();\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-clock-o\\" aria-hidden=\\"true\\" title=\\"Reminder Serial\\" onclick=\\"ReminderEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027)\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-circle\\" aria-hidden=\\"true\\" title=\\"Record Serial\\" onclick=\\"RecordingEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027,30000000550913679)\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e"}'
it('can generate valid url', () => {
expect(url).toBe(
'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram'
)
})
it('can generate valid request data', () => {
const result = request.data({ channel, date })
expect(result).toMatchObject({
Channelarr: '10000000075992337',
fromdate: '202111050000',
todate: '202111060000'
})
})
it('can parse response', () => {
const result = parser({ date, channel, content })
expect(result).toMatchObject([
{
start: 'Thu, 04 Nov 2021 18:30:00 GMT',
stop: 'Thu, 04 Nov 2021 18:54:00 GMT',
title: 'Cynthia Williams - Diwali Look Part 01'
}
])
})
it('can handle empty guide', () => {
const result = parser({ date, channel, content: '{"d":""}' })
expect(result).toMatchObject([])
})

View file

@ -1,104 +1,104 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules'
module.exports = {
site: 'dsmart.com.tr',
days: 2,
url({ date, channel }) {
const [page] = channel.site_id.split('#')
return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}`
},
parser: function ({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
const prev = programs[programs.length - 1]
let start
if (prev) {
start = parseStart(item, prev.stop)
} else {
start = parseStart(item, dayjs.utc(item.day))
}
let duration = parseDuration(item)
let stop = start.add(duration, 's')
programs.push({
title: item.program_name,
category: parseCategory(item),
description: item.description.trim(),
start,
stop
})
})
return programs
},
async channels() {
const perPage = 1
const totalChannels = 210
const pages = Math.ceil(totalChannels / perPage)
const channels = []
for (let i in Array(pages).fill(0)) {
const page = parseInt(i) + 1
const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format(
'YYYY-MM-DD'
)}`
let offset = i * perPage
await axios
.get(url)
.then(r => r.data)
.then(data => {
offset++
if (data && data.data && Array.isArray(data.data.channels)) {
data.data.channels.forEach((item, j) => {
const index = offset + j
channels.push({
lang: 'tr',
name: item.channel_name,
site_id: index + '#' + item._id
})
})
}
})
.catch(err => {
console.log(err.message)
})
}
return channels
}
}
function parseCategory(item) {
return item.genre !== '0' ? item.genre : null
}
function parseStart(item, date) {
const time = dayjs.utc(item.start_date)
return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss')
}
function parseDuration(item) {
const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/)
return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss)
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !data.data || !Array.isArray(data.data.channels)) return null
const channelData = data.data.channels.find(i => i._id == channelId)
return channelData && Array.isArray(channelData.schedule) ? channelData.schedule : []
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules'
module.exports = {
site: 'dsmart.com.tr',
days: 2,
url({ date, channel }) {
const [page] = channel.site_id.split('#')
return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}`
},
parser: function ({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
items.forEach(item => {
const prev = programs[programs.length - 1]
let start
if (prev) {
start = parseStart(item, prev.stop)
} else {
start = parseStart(item, dayjs.utc(item.day))
}
let duration = parseDuration(item)
let stop = start.add(duration, 's')
programs.push({
title: item.program_name,
category: parseCategory(item),
description: item.description.trim(),
start,
stop
})
})
return programs
},
async channels() {
const perPage = 1
const totalChannels = 210
const pages = Math.ceil(totalChannels / perPage)
const channels = []
for (let i in Array(pages).fill(0)) {
const page = parseInt(i) + 1
const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format(
'YYYY-MM-DD'
)}`
let offset = i * perPage
await axios
.get(url)
.then(r => r.data)
.then(data => {
offset++
if (data && data.data && Array.isArray(data.data.channels)) {
data.data.channels.forEach((item, j) => {
const index = offset + j
channels.push({
lang: 'tr',
name: item.channel_name,
site_id: index + '#' + item._id
})
})
}
})
.catch(err => {
console.log(err.message)
})
}
return channels
}
}
function parseCategory(item) {
return item.genre !== '0' ? item.genre : null
}
function parseStart(item, date) {
const time = dayjs.utc(item.start_date)
return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss')
}
function parseDuration(item) {
const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/)
return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss)
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !data.data || !Array.isArray(data.data.channels)) return null
const channelData = data.data.channels.find(i => i._id == channelId)
return channelData && Array.isArray(channelData.schedule) ? channelData.schedule : []
}

View file

@ -1,68 +1,68 @@
// npm run channels:parse -- --config=./sites/dsmart.com.tr/dsmart.com.tr.config.js --output=./sites/dsmart.com.tr/dsmart.com.tr.channels.xml
// npm run grab -- --site=dsmart.com.tr
const { parser, url } = require('./dsmart.com.tr.config.js')
const dayjs = require('dayjs')
const fs = require('fs')
const path = require('path')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '3#5fe07d7acfef0b1593275751',
xmltv_id: 'SinemaTV.tr'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-01-15T22:00:00.000Z',
stop: '2023-01-15T23:45:00.000Z',
title: 'Bizi Ayıran Her Şey',
category: 'sinema/genel',
description:
'Issızlığın ortasında yer alan orta sınıf bir evde bir anne kız yaşamaktadır. Çevrelerindeki taşları insanlarla yaşadıkları çatışmalar, anne-kızın hayatını olumsuz yönde etkilemektedir. Kızının ansızın ortadan kaybolması, bu çatışmaların seviyesini artıracak ve anne, kızını bulmak için her türlü yola başvuracaktır.'
})
expect(results[1]).toMatchObject({
start: '2023-01-15T23:45:00.000Z',
stop: '2023-01-16T01:30:00.000Z',
title: 'Pixie',
category: 'sinema/genel',
description:
'Annesinin intikamını almak isteyen Pixie, dahiyane bir soygun planlar. Fakat işler planladığı gibi gitmeyince kendini İrlandanın vahşi gangsterleri tarafından kovalanan iki adamla birlikte kaçarken bulur.'
})
expect(results[12]).toMatchObject({
start: '2023-01-16T20:30:00.000Z',
stop: '2023-01-16T22:30:00.000Z',
title: 'Seberg',
category: 'sinema/genel',
description:
'Başrolünde ünlü yıldız Kristen Stewartın yer aldığı politik gerilim, 1960ların sonunda insan hakları aktivisti Hakim Jamal ile yaşadığı politik ve romantik ilişki sebebiyle FBI tarafından hedef alınan, Fransız Yeni Dalgasının sevilen yüzü ve Serseri Aşıkların yıldızı Jean Sebergün çarpıcı hikayesini anlatıyor.'
})
})
it('can handle empty guide', () => {
const results = parser({
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(results).toMatchObject([])
})
// npm run channels:parse -- --config=./sites/dsmart.com.tr/dsmart.com.tr.config.js --output=./sites/dsmart.com.tr/dsmart.com.tr.channels.xml
// npm run grab -- --site=dsmart.com.tr
const { parser, url } = require('./dsmart.com.tr.config.js')
const dayjs = require('dayjs')
const fs = require('fs')
const path = require('path')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '3#5fe07d7acfef0b1593275751',
xmltv_id: 'SinemaTV.tr'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ channel, content }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-01-15T22:00:00.000Z',
stop: '2023-01-15T23:45:00.000Z',
title: 'Bizi Ayıran Her Şey',
category: 'sinema/genel',
description:
'Issızlığın ortasında yer alan orta sınıf bir evde bir anne kız yaşamaktadır. Çevrelerindeki taşları insanlarla yaşadıkları çatışmalar, anne-kızın hayatını olumsuz yönde etkilemektedir. Kızının ansızın ortadan kaybolması, bu çatışmaların seviyesini artıracak ve anne, kızını bulmak için her türlü yola başvuracaktır.'
})
expect(results[1]).toMatchObject({
start: '2023-01-15T23:45:00.000Z',
stop: '2023-01-16T01:30:00.000Z',
title: 'Pixie',
category: 'sinema/genel',
description:
'Annesinin intikamını almak isteyen Pixie, dahiyane bir soygun planlar. Fakat işler planladığı gibi gitmeyince kendini İrlandanın vahşi gangsterleri tarafından kovalanan iki adamla birlikte kaçarken bulur.'
})
expect(results[12]).toMatchObject({
start: '2023-01-16T20:30:00.000Z',
stop: '2023-01-16T22:30:00.000Z',
title: 'Seberg',
category: 'sinema/genel',
description:
'Başrolünde ünlü yıldız Kristen Stewartın yer aldığı politik gerilim, 1960ların sonunda insan hakları aktivisti Hakim Jamal ile yaşadığı politik ve romantik ilişki sebebiyle FBI tarafından hedef alınan, Fransız Yeni Dalgasının sevilen yüzü ve Serseri Aşıkların yıldızı Jean Sebergün çarpıcı hikayesini anlatıyor.'
})
})
it('can handle empty guide', () => {
const results = parser({
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(results).toMatchObject([])
})

View file

@ -1,101 +1,101 @@
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide'
module.exports = {
site: 'dstv.com',
days: 2,
request: {
cache: {
ttl: 3 * 60 * 60 * 1000, // 3h
interpretHeader: false
}
},
url: function ({ channel, date }) {
const [region] = channel.site_id.split('#')
const packageName = region === 'nga' ? '&package=DStv%20Premium' : ''
return `${API_ENDPOINT}/GetProgrammes?d=${date.format(
'YYYY-MM-DD'
)}${packageName}&country=${region}`
},
async parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
for (const item of items) {
const details = await loadProgramDetails(item)
programs.push({
title: item.Title,
description: parseDescription(details),
icon: parseIcon(details),
category: parseCategory(details),
start: parseTime(item.StartTime, channel),
stop: parseTime(item.EndTime, channel)
})
}
return programs
},
async channels({ country }) {
const data = await axios
.get(`${API_ENDPOINT}/GetProgrammes?d=2022-03-10&package=DStv%20Premium&country=${country}`)
.then(r => r.data)
.catch(console.log)
return data.Channels.map(item => {
return {
site_id: `${country}#${item.Number}`,
name: item.Name
}
})
}
}
function parseTime(time, channel) {
const [region] = channel.site_id.split('#')
const tz = {
zaf: 'Africa/Johannesburg',
nga: 'Africa/Lagos'
}
return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region])
}
function parseDescription(details) {
return details ? details.Synopsis : null
}
function parseIcon(details) {
return details ? details.ThumbnailUri : null
}
function parseCategory(details) {
return details ? details.SubGenres : null
}
async function loadProgramDetails(item) {
const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}`
return axios
.get(url)
.then(r => r.data)
.catch(console.error)
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !Array.isArray(data.Channels)) return []
const channelData = data.Channels.find(c => c.Number === channelId)
if (!channelData || !Array.isArray(channelData.Programmes)) return []
return channelData.Programmes
}
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide'
module.exports = {
site: 'dstv.com',
days: 2,
request: {
cache: {
ttl: 3 * 60 * 60 * 1000, // 3h
interpretHeader: false
}
},
url: function ({ channel, date }) {
const [region] = channel.site_id.split('#')
const packageName = region === 'nga' ? '&package=DStv%20Premium' : ''
return `${API_ENDPOINT}/GetProgrammes?d=${date.format(
'YYYY-MM-DD'
)}${packageName}&country=${region}`
},
async parser({ content, channel }) {
let programs = []
const items = parseItems(content, channel)
for (const item of items) {
const details = await loadProgramDetails(item)
programs.push({
title: item.Title,
description: parseDescription(details),
icon: parseIcon(details),
category: parseCategory(details),
start: parseTime(item.StartTime, channel),
stop: parseTime(item.EndTime, channel)
})
}
return programs
},
async channels({ country }) {
const data = await axios
.get(`${API_ENDPOINT}/GetProgrammes?d=2022-03-10&package=DStv%20Premium&country=${country}`)
.then(r => r.data)
.catch(console.log)
return data.Channels.map(item => {
return {
site_id: `${country}#${item.Number}`,
name: item.Name
}
})
}
}
function parseTime(time, channel) {
const [region] = channel.site_id.split('#')
const tz = {
zaf: 'Africa/Johannesburg',
nga: 'Africa/Lagos'
}
return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region])
}
function parseDescription(details) {
return details ? details.Synopsis : null
}
function parseIcon(details) {
return details ? details.ThumbnailUri : null
}
function parseCategory(details) {
return details ? details.SubGenres : null
}
async function loadProgramDetails(item) {
const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}`
return axios
.get(url)
.then(r => r.data)
.catch(console.error)
}
function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content)
if (!data || !Array.isArray(data.Channels)) return []
const channelData = data.Channels.find(c => c.Number === channelId)
if (!channelData || !Array.isArray(channelData.Programmes)) return []
return channelData.Programmes
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,89 +1,89 @@
const axios = require('axios')
const { DateTime } = require('luxon')
const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel'
module.exports = {
site: 'epg.i-cable.com',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1h
}
},
url: function ({ channel, date }) {
return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api`
},
parser({ content, channel, date }) {
const programs = []
const items = parseItems(content, date)
for (let item of items) {
const prev = programs[programs.length - 1]
let start = parseStart(item, date)
const stop = start.plus({ minutes: 30 })
if (prev) {
if (start < prev.start) {
start = start.plus({ days: 1 })
date = date.add(1, 'd')
}
prev.stop = start
}
programs.push({
title: parseTitle(item, channel),
start,
stop
})
}
return programs
},
async channels({ lang }) {
const data = await axios
.get(`${API_ENDPOINT}/category/0?api=api`)
.then(r => r.data)
.catch(console.error)
let channels = []
const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`))
await Promise.allSettled(promises).then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') {
channels = channels.concat(r.value.data.chs)
}
})
})
return channels.map(c => {
let name = lang === 'zh' ? c.channel_name : c.channel_name_en
name = c.remark_id == 3 ? `${name} [HD]` : name
return {
site_id: c.channel_no,
name,
lang
}
})
}
}
function parseTitle(item, channel) {
return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi
}
function parseStart(item, date) {
let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM'
return DateTime.fromFormat(
`${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`,
'yyyy-MM-dd hh:mm a',
{
zone: 'Asia/Hong_Kong'
}
).toUTC()
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !Array.isArray(data.epgs)) return []
return data.epgs
}
const axios = require('axios')
const { DateTime } = require('luxon')
const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel'
module.exports = {
site: 'epg.i-cable.com',
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1h
}
},
url: function ({ channel, date }) {
return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api`
},
parser({ content, channel, date }) {
const programs = []
const items = parseItems(content, date)
for (let item of items) {
const prev = programs[programs.length - 1]
let start = parseStart(item, date)
const stop = start.plus({ minutes: 30 })
if (prev) {
if (start < prev.start) {
start = start.plus({ days: 1 })
date = date.add(1, 'd')
}
prev.stop = start
}
programs.push({
title: parseTitle(item, channel),
start,
stop
})
}
return programs
},
async channels({ lang }) {
const data = await axios
.get(`${API_ENDPOINT}/category/0?api=api`)
.then(r => r.data)
.catch(console.error)
let channels = []
const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`))
await Promise.allSettled(promises).then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') {
channels = channels.concat(r.value.data.chs)
}
})
})
return channels.map(c => {
let name = lang === 'zh' ? c.channel_name : c.channel_name_en
name = c.remark_id == 3 ? `${name} [HD]` : name
return {
site_id: c.channel_no,
name,
lang
}
})
}
}
function parseTitle(item, channel) {
return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi
}
function parseStart(item, date) {
let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM'
return DateTime.fromFormat(
`${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`,
'yyyy-MM-dd hh:mm a',
{
zone: 'Asia/Hong_Kong'
}
).toUTC()
}
function parseItems(content) {
const data = JSON.parse(content)
if (!data || !Array.isArray(data.epgs)) return []
return data.epgs
}

View file

@ -1,73 +1,73 @@
// npm run channels:parse -- --config=./sites/epg.i-cable.com/epg.i-cable.com.config.js --output=./sites/epg.i-cable.com/epg.i-cable.com.channels.xml --set=lang:zh
// npm run grab -- --site=epg.i-cable.com
const { parser, url } = require('./epg.i-cable.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '003',
xmltv_id: 'HOYTV.hk',
lang: 'zh'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-14T22:00:00.000Z',
stop: '2022-11-14T23:00:00.000Z',
title: 'Bloomberg 時段'
})
expect(results[31]).toMatchObject({
start: '2022-11-15T21:00:00.000Z',
stop: '2022-11-15T21:30:00.000Z',
title: 'Bloomberg 時段'
})
})
it('can parse response in English', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const channelEN = { ...channel, lang: 'en' }
let results = parser({ content, channel: channelEN, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-14T22:00:00.000Z',
stop: '2022-11-14T23:00:00.000Z',
title: 'Bloomberg Hour'
})
})
it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const results = parser({ date, channel, content })
expect(results).toMatchObject([])
})
// npm run channels:parse -- --config=./sites/epg.i-cable.com/epg.i-cable.com.config.js --output=./sites/epg.i-cable.com/epg.i-cable.com.channels.xml --set=lang:zh
// npm run grab -- --site=epg.i-cable.com
const { parser, url } = require('./epg.i-cable.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
jest.mock('axios')
const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '003',
xmltv_id: 'HOYTV.hk',
lang: 'zh'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-14T22:00:00.000Z',
stop: '2022-11-14T23:00:00.000Z',
title: 'Bloomberg 時段'
})
expect(results[31]).toMatchObject({
start: '2022-11-15T21:00:00.000Z',
stop: '2022-11-15T21:30:00.000Z',
title: 'Bloomberg 時段'
})
})
it('can parse response in English', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const channelEN = { ...channel, lang: 'en' }
let results = parser({ content, channel: channelEN, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2022-11-14T22:00:00.000Z',
stop: '2022-11-14T23:00:00.000Z',
title: 'Bloomberg Hour'
})
})
it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const results = parser({ date, channel, content })
expect(results).toMatchObject([])
})

View file

@ -1,52 +1,52 @@
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone)
dayjs.extend(utc)
module.exports = {
site: 'firstmedia.com',
days: 1,
url: function ({ channel, date }) {
return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${
channel.site_id
}&start_time=1&end_time=24&need_channels=0`
},
parser: function ({ content, channel }) {
if (!content || !channel) return []
let programs = []
const items = parseItems(content, channel.site_id)
items.forEach(item => {
programs.push({
title: parseTitle(item),
description: parseDescription(item),
start: parseStart(item).toISOString(),
stop: parseStop(item).toISOString()
})
})
return programs
}
}
function parseItems(content, channel) {
return JSON.parse(content.trim()).entries[channel]
}
function parseTitle(item) {
return item.title
}
function parseDescription(item) {
return item.long_description
}
function parseStart(item) {
return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta')
}
function parseStop(item) {
return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta')
}
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone)
dayjs.extend(utc)
module.exports = {
site: 'firstmedia.com',
days: 1,
url: function ({ channel, date }) {
return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${
channel.site_id
}&start_time=1&end_time=24&need_channels=0`
},
parser: function ({ content, channel }) {
if (!content || !channel) return []
let programs = []
const items = parseItems(content, channel.site_id)
items.forEach(item => {
programs.push({
title: parseTitle(item),
description: parseDescription(item),
start: parseStart(item).toISOString(),
stop: parseStop(item).toISOString()
})
})
return programs
}
}
function parseItems(content, channel) {
return JSON.parse(content.trim()).entries[channel]
}
function parseTitle(item) {
return item.title
}
function parseDescription(item) {
return item.long_description
}
function parseStart(item) {
return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta')
}
function parseStop(item) {
return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta')
}

View file

@ -1,35 +1,35 @@
const { url, parser } = require('./firstmedia.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.firstmedia.com/ajax/schedule?date=18/06/2023&channel=251&start_time=1&end_time=24&need_channels=0'
)
})
it('can parse response', () => {
const content =
'{"entries":{"251":[{"logo":"files/images/d/new-logo/channels/11-NEWS/ABC Australia SD-FirstMedia-Chl-251.jpg","name":"ABC Australia","id":"2a800e8a-fdcc-47b3-a4a6-58d1d122b326","channel_id":"a1840c59-6c92-8233-3a02-230246aae0c4","channel_no":251,"programme_id":null,"episode":null,"title":"China Tonight","slug":null,"date":"2023-06-13 00:00:00","start_time":"2023-06-13 10:55:00","end_time":"2023-06-13 11:30:00","length":2100,"description":"China Tonight","long_description":"China is a superpower that dominates global news but it\'s also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today.","status":"0","created_by":null,"updated_by":null,"created_at":"2023-06-13 00:20:24","updated_at":"2023-06-13 00:20:24"}]}}'
const results = parser({ content, channel })
expect(results).toMatchObject([
{
start: '2023-06-13T03:55:00.000Z',
stop: '2023-06-13T04:30:00.000Z',
title: 'China Tonight',
description:
"China is a superpower that dominates global news but it's also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today."
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})
const { url, parser } = require('./firstmedia.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' }
it('can generate valid url', () => {
expect(url({ channel, date })).toBe(
'https://www.firstmedia.com/ajax/schedule?date=18/06/2023&channel=251&start_time=1&end_time=24&need_channels=0'
)
})
it('can parse response', () => {
const content =
'{"entries":{"251":[{"logo":"files/images/d/new-logo/channels/11-NEWS/ABC Australia SD-FirstMedia-Chl-251.jpg","name":"ABC Australia","id":"2a800e8a-fdcc-47b3-a4a6-58d1d122b326","channel_id":"a1840c59-6c92-8233-3a02-230246aae0c4","channel_no":251,"programme_id":null,"episode":null,"title":"China Tonight","slug":null,"date":"2023-06-13 00:00:00","start_time":"2023-06-13 10:55:00","end_time":"2023-06-13 11:30:00","length":2100,"description":"China Tonight","long_description":"China is a superpower that dominates global news but it\'s also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today.","status":"0","created_by":null,"updated_by":null,"created_at":"2023-06-13 00:20:24","updated_at":"2023-06-13 00:20:24"}]}}'
const results = parser({ content, channel })
expect(results).toMatchObject([
{
start: '2023-06-13T03:55:00.000Z',
stop: '2023-06-13T04:30:00.000Z',
title: 'China Tonight',
description:
"China is a superpower that dominates global news but it's also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today."
}
])
})
it('can handle empty guide', () => {
const results = parser({ content: '' })
expect(results).toMatchObject([])
})

View file

@ -1,45 +1,45 @@
const dayjs = require('dayjs')
module.exports = {
site: 'flixed.io',
days: 1, // NOTE: changing the date in a request does not change the response
url: function ({ date, channel }) {
return `https://tv-guide.vercel.app/api/stationAirings?stationId=${
channel.site_id
}&startDateTime=${date.toJSON()}`
},
parser({ content }) {
let programs = []
let items = parseItems(content)
items.forEach(item => {
programs.push({
title: item.program.title,
description: item.program.longDescription,
category: item.program.subType,
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseIcon(item) {
const uri = item.program.preferredImage.uri
return uri ? `https://adma.tmsimg.com/assets/${uri}` : null
}
function parseStart(item) {
return dayjs(item.startTime)
}
function parseStop(item) {
return dayjs(item.endTime)
}
function parseItems(content) {
return JSON.parse(content)
}
const dayjs = require('dayjs')
module.exports = {
site: 'flixed.io',
days: 1, // NOTE: changing the date in a request does not change the response
url: function ({ date, channel }) {
return `https://tv-guide.vercel.app/api/stationAirings?stationId=${
channel.site_id
}&startDateTime=${date.toJSON()}`
},
parser({ content }) {
let programs = []
let items = parseItems(content)
items.forEach(item => {
programs.push({
title: item.program.title,
description: item.program.longDescription,
category: item.program.subType,
icon: parseIcon(item),
start: parseStart(item),
stop: parseStop(item)
})
})
return programs
}
}
function parseIcon(item) {
const uri = item.program.preferredImage.uri
return uri ? `https://adma.tmsimg.com/assets/${uri}` : null
}
function parseStart(item) {
return dayjs(item.startTime)
}
function parseStop(item) {
return dayjs(item.endTime)
}
function parseItems(content) {
return JSON.parse(content)
}

View file

@ -1,49 +1,49 @@
// npm run grab -- --site=flixed.io
const { parser, url } = require('./flixed.io.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '108970',
xmltv_id: 'VSiN.us'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://tv-guide.vercel.app/api/stationAirings?stationId=108970&startDateTime=2023-01-19T00:00:00.000Z'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-01-19T05:00:00.000Z',
stop: '2023-01-19T06:00:00.000Z',
title: 'The Greg Peterson Experience',
category: 'Sports non-event',
icon: 'https://adma.tmsimg.com/assets/assets/p20628892_b_v13_aa.jpg?w=270&h=360',
description: 'A different kind of sports betting.'
})
})
it('can handle empty guide', () => {
const results = parser({
content: '[]'
})
expect(results).toMatchObject([])
})
// npm run grab -- --site=flixed.io
const { parser, url } = require('./flixed.io.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '108970',
xmltv_id: 'VSiN.us'
}
it('can generate valid url', () => {
expect(url({ date, channel })).toBe(
'https://tv-guide.vercel.app/api/stationAirings?stationId=108970&startDateTime=2023-01-19T00:00:00.000Z'
)
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date })
results = results.map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results[0]).toMatchObject({
start: '2023-01-19T05:00:00.000Z',
stop: '2023-01-19T06:00:00.000Z',
title: 'The Greg Peterson Experience',
category: 'Sports non-event',
icon: 'https://adma.tmsimg.com/assets/assets/p20628892_b_v13_aa.jpg?w=270&h=360',
description: 'A different kind of sports betting.'
})
})
it('can handle empty guide', () => {
const results = parser({
content: '[]'
})
expect(results).toMatchObject([])
})

View file

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

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