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 { Logger } from '@freearhey/core'
import { ApiClient } from '../../core' import { ApiClient } from '../../core'
async function main() { async function main() {
const logger = new Logger() const logger = new Logger()
const client = new ApiClient({ logger }) const client = new ApiClient({ logger })
const requests = [ const requests = [
client.download('channels.json'), client.download('channels.json'),
client.download('countries.json'), client.download('countries.json'),
client.download('regions.json'), client.download('regions.json'),
client.download('subdivisions.json') client.download('subdivisions.json')
] ]
await Promise.all(requests) await Promise.all(requests)
} }
main() main()

View file

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

View file

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

View file

@ -1,81 +1,85 @@
import { Logger, File, Collection, Storage } from '@freearhey/core' import { Logger, File, Collection, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core' import { ChannelsParser, XML } from '../../core'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
import { Command, OptionValues } from 'commander' import { Command } from 'commander'
import path from 'path' import path from 'path'
const program = new Command() const program = new Command()
program program
.requiredOption('-c, --config <config>', 'Config file') .requiredOption('-c, --config <config>', 'Config file')
.option('-s, --set [args...]', 'Set custom arguments') .option('-s, --set [args...]', 'Set custom arguments')
.option('-o, --output <output>', 'Output file') .option('-o, --output <output>', 'Output file')
.option('--clean', 'Delete the previous *.channels.xml if exists') .option('--clean', 'Delete the previous *.channels.xml if exists')
.parse(process.argv) .parse(process.argv)
type ParseOptions = { type ParseOptions = {
config: string config: string
set?: string set?: string
output?: string output?: string
clean?: boolean clean?: boolean
} }
const options: ParseOptions = program.opts() const options: ParseOptions = program.opts()
async function main() { async function main() {
const storage = new Storage() const storage = new Storage()
const parser = new ChannelsParser({ storage }) const parser = new ChannelsParser({ storage })
const logger = new Logger() const logger = new Logger()
const file = new File(options.config) const file = new File(options.config)
const dir = file.dirname() const dir = file.dirname()
const config = require(path.resolve(options.config)) const config = require(path.resolve(options.config))
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
let channels = new Collection() let channels = new Collection()
if (!options.clean && (await storage.exists(outputFilepath))) { if (!options.clean && (await storage.exists(outputFilepath))) {
channels = await parser.parse(outputFilepath) channels = await parser.parse(outputFilepath)
} }
const args: { const args: {
[key: string]: any [key: string]: string
} = {} } = {}
if (Array.isArray(options.set)) { if (Array.isArray(options.set)) {
options.set.forEach((arg: string) => { options.set.forEach((arg: string) => {
const [key, value] = arg.split(':') const [key, value] = arg.split(':')
args[key] = value args[key] = value
}) })
} }
let parsedChannels = config.channels(args) let parsedChannels = config.channels(args)
if (isPromise(parsedChannels)) { if (isPromise(parsedChannels)) {
parsedChannels = await parsedChannels parsedChannels = await parsedChannels
} }
parsedChannels = parsedChannels.map((channel: Channel) => { parsedChannels = parsedChannels.map((channel: Channel) => {
channel.site = config.site channel.site = config.site
return channel return channel
}) })
channels = channels channels = channels
.mergeBy( .mergeBy(
new Collection(parsedChannels), new Collection(parsedChannels),
(channel: Channel) => channel.site_id.toString() + channel.lang (channel: Channel) => channel.site_id.toString() + channel.lang
) )
.orderBy([ .orderBy([
(channel: Channel) => channel.lang, (channel: Channel) => channel.lang,
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'), (channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'),
(channel: Channel) => channel.site_id (channel: Channel) => channel.site_id
]) ])
const xml = new XML(channels) const xml = new XML(channels)
await storage.save(outputFilepath, xml.toString()) await storage.save(outputFilepath, xml.toString())
logger.info(`File '${outputFilepath}' successfully saved`) logger.info(`File '${outputFilepath}' successfully saved`)
} }
main() main()
function isPromise(promise: any) { function isPromise(promise: object[] | Promise<object[]>) {
return !!promise && typeof promise.then === 'function' 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 { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core'
import { ChannelsParser, ApiChannel } from '../../core' import { ChannelsParser, ApiChannel } from '../../core'
import { program } from 'commander' import { program } from 'commander'
import chalk from 'chalk' import chalk from 'chalk'
import langs from 'langs' import langs from 'langs'
import { DATA_DIR } from '../../constants' import { DATA_DIR } from '../../constants'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
program program
.option( .option(
'-c, --channels <path>', '-c, --channels <path>',
'Path to channels.xml file to validate', 'Path to channels.xml file to validate',
'sites/**/*.channels.xml' 'sites/**/*.channels.xml'
) )
.parse(process.argv) .parse(process.argv)
const options = program.opts() const options = program.opts()
type ValidationError = { type ValidationError = {
type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang' type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang'
name: string name: string
lang?: string lang?: string
xmltv_id?: string xmltv_id?: string
site_id?: string site_id?: string
logo?: string logo?: string
} }
async function main() { async function main() {
const logger = new Logger() const logger = new Logger()
logger.info('options:') logger.info('options:')
logger.tree(options) logger.tree(options)
const parser = new ChannelsParser({ storage: new Storage() }) const parser = new ChannelsParser({ storage: new Storage() })
const dataStorage = new Storage(DATA_DIR) const dataStorage = new Storage(DATA_DIR)
const channelsContent = await dataStorage.json('channels.json') const channelsContent = await dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
let totalFiles = 0 let totalFiles = 0
let totalErrors = 0 let totalErrors = 0
const storage = new Storage() const storage = new Storage()
let files: string[] = await storage.list(options.channels) const files: string[] = await storage.list(options.channels)
for (const filepath of files) { for (const filepath of files) {
const file = new File(filepath) const file = new File(filepath)
if (file.extension() !== 'xml') continue if (file.extension() !== 'xml') continue
const parsedChannels = await parser.parse(filepath) const parsedChannels = await parser.parse(filepath)
const bufferById = new Dictionary() const bufferById = new Dictionary()
const bufferBySiteId = new Dictionary() const bufferBySiteId = new Dictionary()
const errors: ValidationError[] = [] const errors: ValidationError[] = []
parsedChannels.forEach((channel: Channel) => { parsedChannels.forEach((channel: Channel) => {
const bufferId: string = `${channel.xmltv_id}:${channel.lang}` const bufferId: string = `${channel.xmltv_id}:${channel.lang}`
if (bufferById.missing(bufferId)) { if (bufferById.missing(bufferId)) {
bufferById.set(bufferId, true) bufferById.set(bufferId, true)
} else { } else {
errors.push({ type: 'duplicate', ...channel }) errors.push({ type: 'duplicate', ...channel })
totalErrors++ totalErrors++
} }
const bufferSiteId: string = `${channel.site_id}:${channel.lang}` const bufferSiteId: string = `${channel.site_id}:${channel.lang}`
if (bufferBySiteId.missing(bufferSiteId)) { if (bufferBySiteId.missing(bufferSiteId)) {
bufferBySiteId.set(bufferSiteId, true) bufferBySiteId.set(bufferSiteId, true)
} else { } else {
errors.push({ type: 'duplicate', ...channel }) errors.push({ type: 'duplicate', ...channel })
totalErrors++ totalErrors++
} }
if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) { if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) {
errors.push({ type: 'wrong_xmltv_id', ...channel }) errors.push({ type: 'wrong_xmltv_id', ...channel })
totalErrors++ totalErrors++
} }
if (!langs.where('1', channel.lang)) { if (!langs.where('1', channel.lang)) {
errors.push({ type: 'wrong_lang', ...channel }) errors.push({ type: 'wrong_lang', ...channel })
totalErrors++ totalErrors++
} }
}) })
if (errors.length) { if (errors.length) {
console.log(chalk.underline(filepath)) console.log(chalk.underline(filepath))
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name']) console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
console.log() console.log()
totalFiles++ totalFiles++
} }
} }
if (totalErrors > 0) { if (totalErrors > 0) {
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`)) console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
process.exit(1) process.exit(1)
} }
} }
main() main()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,28 +1,28 @@
import { DateTime, Collection } from '@freearhey/core' import { DateTime, Collection } from '@freearhey/core'
import { generateXMLTV } from 'epg-grabber' import { generateXMLTV } from 'epg-grabber'
type XMLTVProps = { type XMLTVProps = {
channels: Collection channels: Collection
programs: Collection programs: Collection
date: DateTime date: DateTime
} }
export class XMLTV { export class XMLTV {
channels: Collection channels: Collection
programs: Collection programs: Collection
date: DateTime date: DateTime
constructor({ channels, programs, date }: XMLTVProps) { constructor({ channels, programs, date }: XMLTVProps) {
this.channels = channels this.channels = channels
this.programs = programs this.programs = programs
this.date = date this.date = date
} }
toString() { toString() {
return generateXMLTV({ return generateXMLTV({
channels: this.channels.all(), channels: this.channels.all(),
programs: this.programs.all(), programs: this.programs.all(),
date: this.date.toJSON() 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 cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: '9tv.co.il', site: '9tv.co.il',
days: 2, days: 2,
url: function ({ date }) { url: function ({ date }) {
return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format(
'DD/MM/YYYY 00:00:00' 'DD/MM/YYYY 00:00:00'
)}` )}`
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
const start = parseStart($item, date) const start = parseStart($item, date)
if (prev) prev.stop = start if (prev) prev.stop = start
const stop = start.add(1, 'h') const stop = start.add(1, 'h')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
icon: parseIcon($item), icon: parseIcon($item),
description: parseDescription($item), description: parseDescription($item),
start, start,
stop stop
}) })
}) })
return programs return programs
} }
} }
function parseStart($item, date) { function parseStart($item, date) {
let time = $item('a > div.guide_list_time').text().trim() 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') return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem')
} }
function parseIcon($item) { function parseIcon($item) {
const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css(
'background-image' 'background-image'
) )
if (!backgroundImage) return null if (!backgroundImage) return null
const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null]
return relativePath ? `https://www.9tv.co.il${relativePath}` : null return relativePath ? `https://www.9tv.co.il${relativePath}` : null
} }
function parseDescription($item) { function parseDescription($item) {
return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim()
} }
function parseTitle($item) { function parseTitle($item) {
return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim()
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('li').toArray() return $('li').toArray()
} }

View file

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

View file

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

View file

@ -1,56 +1,56 @@
// npm run grab -- --site=abc.net.au // npm run grab -- --site=abc.net.au
const { parser, url } = require('./abc.net.au.config.js') const { parser, url } = require('./abc.net.au.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'ABC1', site_id: 'ABC1',
xmltv_id: 'ABCTV.au' xmltv_id: 'ABCTV.au'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json') expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = 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}]}]}' '{"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 => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: 'Silent Witness', title: 'Silent Witness',
sub_title: 'Lift Up Your Hearts (part Two)', sub_title: 'Lift Up Your Hearts (part Two)',
description: 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?', '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'], category: ['Entertainment'],
rating: { rating: {
system: 'ACB', system: 'ACB',
value: 'M' value: 'M'
}, },
season: 22, season: 22,
episode: 4, episode: 4,
icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg', icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg',
start: '2022-12-21T13:46:00.000Z', start: '2022-12-21T13:46:00.000Z',
stop: '2022-12-21T14:44:00.000Z' stop: '2022-12-21T14:44:00.000Z'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser( const result = parser(
{ {
content: 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>' '<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 channel
) )
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,63 +1,63 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'allente.se', site: 'allente.se',
days: 2, days: 2,
url({ date, channel }) { url({ date, channel }) {
const [country] = channel.site_id.split('#') const [country] = channel.site_id.split('#')
return `https://cs-vcb.allente.${country}/epg/events?date=${date.format('YYYY-MM-DD')}` return `https://cs-vcb.allente.${country}/epg/events?date=${date.format('YYYY-MM-DD')}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
if (!item.details) return if (!item.details) return
const start = dayjs(item.time) const start = dayjs(item.time)
const stop = start.add(item.details.duration, 'm') const stop = start.add(item.details.duration, 'm')
programs.push({ programs.push({
title: item.title, title: item.title,
category: item.details.categories, category: item.details.categories,
description: item.details.description, description: item.details.description,
icon: item.details.image, icon: item.details.image,
season: parseSeason(item), season: parseSeason(item),
episode: parseEpisode(item), episode: parseEpisode(item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels({ country, lang }) { async channels({ country, lang }) {
const data = await axios const data = await axios
.get(`https://cs-vcb.allente.${country}/epg/events?date=2021-11-17`) .get(`https://cs-vcb.allente.${country}/epg/events?date=2021-11-17`)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data.channels.map(item => { return data.channels.map(item => {
return { return {
lang, lang,
site_id: `${country}#${item.id}`, site_id: `${country}#${item.id}`,
name: item.name name: item.name
} }
}) })
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.channels)) return [] if (!data || !Array.isArray(data.channels)) return []
const channelData = data.channels.find(i => i.id === channelId) const channelData = data.channels.find(i => i.id === channelId)
return channelData && Array.isArray(channelData.events) ? channelData.events : [] return channelData && Array.isArray(channelData.events) ? channelData.events : []
} }
function parseSeason(item) { function parseSeason(item) {
return item.details.season || null return item.details.season || null
} }
function parseEpisode(item) { function parseEpisode(item) {
return item.details.episode || null 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_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_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_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 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 // npm run grab -- --site=allente.se
const { parser, url } = require('./allente.se.config.js') const { parser, url } = require('./allente.se.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'se#0148', site_id: 'se#0148',
xmltv_id: 'SVT1.se' xmltv_id: 'SVT1.se'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17') expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17')
}) })
it('can generate valid url for different country', () => { it('can generate valid url for different country', () => {
const dkChannel = { site_id: 'dk#0148' } const dkChannel = { site_id: 'dk#0148' }
expect(url({ date, channel: dkChannel })).toBe( expect(url({ date, channel: dkChannel })).toBe(
'https://cs-vcb.allente.dk/epg/events?date=2021-11-17' 'https://cs-vcb.allente.dk/epg/events?date=2021-11-17'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = 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"}}]}]}' '{"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 => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-08-22T07:10:00.000Z', start: '2022-08-22T07:10:00.000Z',
stop: '2022-08-22T07:30:00.000Z', stop: '2022-08-22T07:30:00.000Z',
title: 'Hemmagympa med Sofia', title: 'Hemmagympa med Sofia',
category: ['other'], category: ['other'],
description: description:
'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.',
icon: 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', 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, season: 4,
episode: 1 episode: 1
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}' content: '{"date":"2001-11-17","categories":[],"channels":[]}'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,130 +1,130 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'beinsports.com', site: 'beinsports.com',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000, // 1h ttl: 60 * 60 * 1000, // 1h
interpretHeader: false interpretHeader: false
} }
}, },
url: function ({ date, channel }) { url: function ({ date, channel }) {
let [region] = channel.site_id.split('#') let [region] = channel.site_id.split('#')
region = region ? `_${region}` : '' region = region ? `_${region}` : ''
return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format( return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}` )}`
}, },
parser: function ({ content, channel, date }) { parser: function ({ content, channel, date }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
let i = 0 let i = 0
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const title = parseTitle($item) const title = parseTitle($item)
if (!title) return if (!title) return
const category = parseCategory($item) const category = parseCategory($item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (i === 0 && start.hour() > 18) { if (i === 0 && start.hour() > 18) {
date = date.subtract(1, 'd') date = date.subtract(1, 'd')
start = start.subtract(1, 'd') start = start.subtract(1, 'd')
} }
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
let stop = parseStop($item, start) let stop = parseStop($item, start)
if (stop.isBefore(start)) { if (stop.isBefore(start)) {
stop = stop.add(1, 'd') stop = stop.add(1, 'd')
} }
programs.push({ title, category, start, stop }) programs.push({ title, category, start, stop })
i++ i++
}) })
return programs return programs
}, },
async channels({ region, lang }) { async channels({ region, lang }) {
const suffix = region ? `_${region}` : '' const suffix = region ? `_${region}` : ''
const content = await axios const content = await axios
.get( .get(
`https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08` `https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08`
) )
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(content) const $ = cheerio.load(content)
const items = $('.container > div, #epg_div > div').toArray() const items = $('.container > div, #epg_div > div').toArray()
return items return items
.map(item => { .map(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const id = $item('*').attr('id') const id = $item('*').attr('id')
if (!/^channels_[0-9]+$/.test(id)) return null if (!/^channels_[0-9]+$/.test(id)) return null
const channelId = id.replace('channels_', '') const channelId = id.replace('channels_', '')
const imgSrc = $item('img').attr('src') const imgSrc = $item('img').attr('src')
const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, ''] const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, '']
return { return {
lang, lang,
site_id: `${region}#${channelId}`, site_id: `${region}#${channelId}`,
name name
} }
}) })
.filter(i => i) .filter(i => i)
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.title').text() return $item('.title').text()
} }
function parseCategory($item) { function parseCategory($item) {
return $item('.format') return $item('.format')
.map(function () { .map(function () {
return $item(this).text() return $item(this).text()
}) })
.get() .get()
} }
function parseStart($item, date) { function parseStart($item, date) {
let time = $item('.time').text() let time = $item('.time').text()
if (!time) return null if (!time) return null
let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null] let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null]
if (!start) return null if (!start) return null
start = `${date.format('YYYY-MM-DD')} ${start}${period}` start = `${date.format('YYYY-MM-DD')} ${start}${period}`
const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm'
return dayjs.tz(start, format, 'Asia/Qatar') return dayjs.tz(start, format, 'Asia/Qatar')
} }
function parseStop($item, date) { function parseStop($item, date) {
let time = $item('.time').text() let time = $item('.time').text()
if (!time) return null if (!time) return null
let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null] let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null]
if (!stop) return null if (!stop) return null
stop = `${date.format('YYYY-MM-DD')} ${stop}${period}` stop = `${date.format('YYYY-MM-DD')} ${stop}${period}`
const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm'
return dayjs.tz(stop, format, 'Asia/Qatar') return dayjs.tz(stop, format, 'Asia/Qatar')
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() 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 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
// npm run grab -- --site=beinsports.com // npm run grab -- --site=beinsports.com
const { parser, url } = require('./beinsports.com.config.js') const { parser, url } = require('./beinsports.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' } const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' 'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08'
) )
}) })
it('can generate valid url for arabic guide', () => { it('can generate valid url for arabic guide', () => {
const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' } const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' }
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' 'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html')) const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html'))
const results = parser({ date, channel, content }).map(p => { const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-05-07T19:30:00.000Z', start: '2022-05-07T19:30:00.000Z',
stop: '2022-05-07T21:20:00.000Z', stop: '2022-05-07T21:20:00.000Z',
title: 'Lorient vs Marseille', title: 'Lorient vs Marseille',
category: ['Ligue 1 2021/22'] category: ['Ligue 1 2021/22']
}) })
}) })
it('can parse response for tomorrow', () => { it('can parse response for tomorrow', () => {
const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d')
const content = fs.readFileSync( const content = fs.readFileSync(
path.resolve('sites/beinsports.com/__data__/content_tomorrow.html') path.resolve('sites/beinsports.com/__data__/content_tomorrow.html')
) )
const results = parser({ date, channel, content }).map(p => { const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-05-08T21:20:00.000Z', start: '2022-05-08T21:20:00.000Z',
stop: '2022-05-08T23:10:00.000Z', stop: '2022-05-08T23:10:00.000Z',
title: 'Celtic vs Hearts', title: 'Celtic vs Hearts',
category: ['SPFL Premiership 2021/22'] category: ['SPFL Premiership 2021/22']
}) })
}) })
it('can parse US response', () => { it('can parse US response', () => {
const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html')) const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html'))
const results = parser({ date, channel, content }).map(p => { const results = parser({ date, channel, content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-05-07T20:00:00.000Z', start: '2022-05-07T20:00:00.000Z',
stop: '2022-05-07T22:00:00.000Z', stop: '2022-05-07T22:00:00.000Z',
title: 'Basaksehir vs. Galatasaray', title: 'Basaksehir vs. Galatasaray',
category: ['Fútbol Turco Superliga', 'Soccer'] category: ['Fútbol Turco Superliga', 'Soccer']
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html')) const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html'))
const result = parser({ const result = parser({
date, date,
channel, channel,
content: noContent content: noContent
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

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

View file

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

View file

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

View file

@ -1,52 +1,52 @@
// npm run grab -- --site=bt.com // npm run grab -- --site=bt.com
const { parser, url } = require('./bt.com.config.js') const { parser, url } = require('./bt.com.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'hsxv', site_id: 'hsxv',
xmltv_id: 'BBCOneHD.uk' xmltv_id: 'BBCOneHD.uk'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( 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' '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', () => { it('can parse response', () => {
const content = 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"}}}' '{"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 => { const result = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: 'The Finest Hours (2016)', title: 'The Finest Hours (2016)',
description: 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.", "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', 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, season: null,
episode: null, episode: null,
start: '2022-03-19T23:30:00.000Z', start: '2022-03-19T23:30:00.000Z',
stop: '2022-03-20T01:20:00.000Z' stop: '2022-03-20T01:20:00.000Z'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: 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"}}}' '{"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([]) expect(result).toMatchObject([])
}) })

View file

@ -1,109 +1,109 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'cablego.com.pe', site: 'cablego.com.pe',
days: 2, days: 2,
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'x-requested-with': 'XMLHttpRequest' 'x-requested-with': 'XMLHttpRequest'
}, },
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ channel, date }) { url({ channel, date }) {
const [page] = channel.site_id.split('#') const [page] = channel.site_id.split('#')
return `https://cablego.com.pe/epg/default/${date.format( return `https://cablego.com.pe/epg/default/${date.format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}?page=${page}&do=loadPage` )}?page=${page}&do=loadPage`
}, },
parser: function ({ content, channel, date }) { parser: function ({ content, channel, date }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (!start) return if (!start) return
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const promises = [0, 1, 2, 3, 4].map(page => { const promises = [0, 1, 2, 3, 4].map(page => {
return axios.post( return axios.post(
`https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`, `https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`,
null, null,
{ {
headers: { headers: {
'x-requested-with': 'XMLHttpRequest' 'x-requested-with': 'XMLHttpRequest'
} }
} }
) )
}) })
const channels = [] const channels = []
await Promise.allSettled(promises).then(results => { await Promise.allSettled(promises).then(results => {
results.forEach((r, page) => { results.forEach((r, page) => {
if (r.status === 'fulfilled') { if (r.status === 'fulfilled') {
const html = r.value.data.snippets['snippet--channelGrid'] const html = r.value.data.snippets['snippet--channelGrid']
const $ = cheerio.load(html) const $ = cheerio.load(html)
$('.epg-channel-strip').each((i, el) => { $('.epg-channel-strip').each((i, el) => {
const channelId = $(el).find('.epg-channel-logo').attr('id') const channelId = $(el).find('.epg-channel-logo').attr('id')
channels.push({ channels.push({
lang: 'es', lang: 'es',
site_id: `${page}#${channelId}`, site_id: `${page}#${channelId}`,
name: $(el).find('img').attr('alt') name: $(el).find('img').attr('alt')
}) })
}) })
} }
}) })
}) })
return channels return channels
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('span:nth-child(2) > a').text().trim() return $item('span:nth-child(2) > a').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('.epg-show-start').text().trim() const time = $item('.epg-show-start').text().trim()
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima') return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima')
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return [] if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return []
const html = data.snippets['snippet--channelGrid'] const html = data.snippets['snippet--channelGrid']
const $ = cheerio.load(html) const $ = cheerio.load(html)
return $(`#${channelId}`).parent().find('.epg-show').toArray() 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 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 // npm run grab -- --site=cablego.com.pe
const { parser, url, request } = require('./cablego.com.pe.config.js') const { parser, url, request } = require('./cablego.com.pe.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '0#LATINA', site_id: '0#LATINA',
xmltv_id: 'Latina.pe' xmltv_id: 'Latina.pe'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage' 'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage'
) )
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'x-requested-with': 'XMLHttpRequest' 'x-requested-with': 'XMLHttpRequest'
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date }).map(p => { let results = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-11-28T05:00:00.000Z', start: '2022-11-28T05:00:00.000Z',
stop: '2022-11-28T06:30:00.000Z', stop: '2022-11-28T06:30:00.000Z',
title: 'Especiales Qatar' title: 'Especiales Qatar'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const result = parser({ content, channel, date }) const result = parser({ content, channel, date })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,133 +1,133 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.reportv.com.ar/finder' const API_ENDPOINT = 'https://www.reportv.com.ar/finder'
module.exports = { module.exports = {
site: 'cableplus.com.uy', site: 'cableplus.com.uy',
days: 2, days: 2,
url: `${API_ENDPOINT}/channel`, url: `${API_ENDPOINT}/channel`,
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}, },
data({ date, channel }) { data({ date, channel }) {
const params = new URLSearchParams() const params = new URLSearchParams()
params.append('idAlineacion', '3017') params.append('idAlineacion', '3017')
params.append('idSenial', channel.site_id) params.append('idSenial', channel.site_id)
params.append('fecha', date.format('YYYY-MM-DD')) params.append('fecha', date.format('YYYY-MM-DD'))
params.append('hora', '00:00') params.append('hora', '00:00')
return params return params
} }
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
categories: parseCategories($item), categories: parseCategories($item),
icon: parseIcon($item), icon: parseIcon($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const params = new URLSearchParams({ idAlineacion: '3017' }) const params = new URLSearchParams({ idAlineacion: '3017' })
const data = await axios const data = await axios
.post(`${API_ENDPOINT}/channelGrid`, params, { .post(`${API_ENDPOINT}/channelGrid`, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
const $ = cheerio.load(data) const $ = cheerio.load(data)
return $('.senial') return $('.senial')
.map(function () { .map(function () {
return { return {
lang: 'es', lang: 'es',
site_id: $(this).attr('id'), site_id: $(this).attr('id'),
name: $(this).find('img').attr('alt') name: $(this).find('img').attr('alt')
} }
}) })
.get() .get()
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child')
.text() .text()
.trim() .trim()
} }
function parseIcon($item) { function parseIcon($item) {
return $item('img').data('src') || $item('img').attr('src') || null return $item('img').data('src') || $item('img').attr('src') || null
} }
function parseCategories($item) { function parseCategories($item) {
return $item('p.evento_genero') return $item('p.evento_genero')
.map(function () { .map(function () {
return $item(this).text().trim() return $item(this).text().trim()
}) })
.toArray() .toArray()
} }
function parseStart($item, date) { function parseStart($item, date) {
let time = $item('.grid_fecha_hora').text().trim() let time = $item('.grid_fecha_hora').text().trim()
if (time) { if (time) {
return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo')
} }
time = $item('.fechaHora').text().trim() time = $item('.fechaHora').text().trim()
return time return time
? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo')
: null : null
} }
function parseItems(content, date) { function parseItems(content, date) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa')
.filter(function () { .filter(function () {
return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1
}) })
.toArray() .toArray()
let otherItems = $('#owl-pc > .item-program') let otherItems = $('#owl-pc > .item-program')
.filter(function () { .filter(function () {
return ( return (
$(this) $(this)
.find('.evento_titulo > .horario > p.fechaHora') .find('.evento_titulo > .horario > p.fechaHora')
.text() .text()
.indexOf(date.format('DD/MM')) > -1 .indexOf(date.format('DD/MM')) > -1
) )
}) })
.toArray() .toArray()
return featuredItems.concat(otherItems) 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 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 // npm run grab -- --site=cableplus.com.uy
const { parser, url, request } = require('./cableplus.com.uy.config.js') const { parser, url, request } = require('./cableplus.com.uy.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '2035', site_id: '2035',
xmltv_id: 'APlusV.uy' xmltv_id: 'APlusV.uy'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://www.reportv.com.ar/finder/channel') expect(url).toBe('https://www.reportv.com.ar/finder/channel')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const params = request.data({ date, channel }) const params = request.data({ date, channel })
expect(params.get('idAlineacion')).toBe('3017') expect(params.get('idAlineacion')).toBe('3017')
expect(params.get('idSenial')).toBe('2035') expect(params.get('idSenial')).toBe('2035')
expect(params.get('fecha')).toBe('2023-02-12') expect(params.get('fecha')).toBe('2023-02-12')
expect(params.get('hora')).toBe('00:00') expect(params.get('hora')).toBe('00:00')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
let results = parser({ content, date }) let results = parser({ content, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(21) expect(results.length).toBe(21)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-02-12T09:30:00.000Z', start: '2023-02-12T09:30:00.000Z',
stop: '2023-02-12T10:30:00.000Z', stop: '2023-02-12T10:30:00.000Z',
title: 'Revista agropecuaria', title: 'Revista agropecuaria',
icon: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', icon: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg',
categories: [] categories: []
}) })
expect(results[4]).toMatchObject({ expect(results[4]).toMatchObject({
start: '2023-02-12T12:30:00.000Z', start: '2023-02-12T12:30:00.000Z',
stop: '2023-02-12T13:30:00.000Z', stop: '2023-02-12T13:30:00.000Z',
title: 'De pago en pago', title: 'De pago en pago',
icon: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', icon: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg',
categories: ['Cultural'] categories: ['Cultural']
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,93 +1,93 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'canalplus-caraibes.com', site: 'canalplus-caraibes.com',
days: 2, days: 2,
url: function ({ channel, date }) { url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd') 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}` return `https://service.canal-overseas.com/ott-frontend/vector/53001/channel/${channel.site_id}/events?filter.day=${diff}`
}, },
async parser({ content }) { async parser({ content }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
for (let item of items) { for (let item of items) {
if (item.title === 'Fin des programmes') return if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item) const detail = await loadProgramDetails(item)
programs.push({ programs.push({
title: item.title, title: item.title,
description: parseDescription(detail), description: parseDescription(detail),
category: parseCategory(detail), category: parseCategory(detail),
icon: parseIcon(item), icon: parseIcon(item),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const html = await axios const html = await axios
.get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir') .get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const script = $('body > script:nth-child(2)').html() const script = $('body > script:nth-child(2)').html()
const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null]
const data = JSON.parse(json) const data = JSON.parse(json)
const items = data.tvGuide.channels.byZapNumber const items = data.tvGuide.channels.byZapNumber
return Object.values(items).map(item => { return Object.values(items).map(item => {
return { return {
lang: 'fr', lang: 'fr',
site_id: item.epgID, site_id: item.epgID,
name: item.name name: item.name
} }
}) })
} }
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {} if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage const url = item.onClick.URLPage
const data = await axios const data = await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data || {} return data || {}
} }
function parseDescription(detail) { function parseDescription(detail) {
return detail.detail.informations.summary || null return detail.detail.informations.summary || null
} }
function parseCategory(detail) { function parseCategory(detail) {
return detail.detail.informations.subGenre || null return detail.detail.informations.subGenre || null
} }
function parseIcon(item) { function parseIcon(item) {
return item.URLImage || item.URLImageDefault return item.URLImage || item.URLImageDefault
} }
function parseStart(item) { function parseStart(item) {
return dayjs.unix(item.startTime) return dayjs.unix(item.startTime)
} }
function parseStop(item) { function parseStop(item) {
return dayjs.unix(item.endTime) return dayjs.unix(item.endTime)
} }
function parseItems(content) { function parseItems(content) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !data.timeSlices) return [] if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => { const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents) acc = acc.concat(curr.contents)
return acc return acc
}, []) }, [])
return items 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 // [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 // npm run grab -- --site=canalplus-caraibes.com
const { parser, url } = require('./canalplus-caraibes.com.config.js') const { parser, url } = require('./canalplus-caraibes.com.config.js')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const channel = { const channel = {
site_id: '50115', site_id: '50115',
xmltv_id: 'beINSports1France.fr' xmltv_id: 'beINSports1France.fr'
} }
it('can generate valid url for today', () => { it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d') const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0' 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0'
) )
}) })
it('can generate valid url for tomorrow', () => { it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd') const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1' 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1'
) )
}) })
it('can parse response', done => { it('can parse response', done => {
const content = 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"}]}' '{"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 => { axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') { if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(`{ data: JSON.parse(`{
"currentPage": { "currentPage": {
"displayName": "Rugby - Leinster / La Rochelle", "displayName": "Rugby - Leinster / La Rochelle",
"displayTemplate": "detailPage", "displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"
}, },
"detail": { "detail": {
"informations": { "informations": {
"programmeType": "EPG", "programmeType": "EPG",
"isInOffer": false, "isInOffer": false,
"isInOfferOnDevice": false, "isInOfferOnDevice": false,
"isInOfferForD2G": false, "isInOfferForD2G": false,
"availableInVoDOnDevice": false, "availableInVoDOnDevice": false,
"availableInVoDOnG5": false, "availableInVoDOnG5": false,
"availableInD2GOnDevice": false, "availableInD2GOnDevice": false,
"availableInLiveOnDevice": false, "availableInLiveOnDevice": false,
"rediffusions": true, "rediffusions": true,
"canBeRecorded": false, "canBeRecorded": false,
"channelName": "BEIN SPORTS 1 HD", "channelName": "BEIN SPORTS 1 HD",
"startTime": 1660815000, "startTime": 1660815000,
"endTime": 1660816800, "endTime": 1660816800,
"title": "Rugby - Leinster / La Rochelle", "title": "Rugby - Leinster / La Rochelle",
"subtitle": "Rugby", "subtitle": "Rugby",
"thirdTitle": "BEIN SPORTS 1 HD", "thirdTitle": "BEIN SPORTS 1 HD",
"genre": "Sport", "genre": "Sport",
"subGenre": "Rugby", "subGenre": "Rugby",
"editorialTitle": "Sport, France, 0h30", "editorialTitle": "Sport, France, 0h30",
"audioLanguage": "VF", "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.", "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.", "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, "programID": 224515801,
"sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", "sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html",
"EpgId": 50115, "EpgId": 50115,
"CSA": 1, "CSA": 1,
"HD": false, "HD": false,
"3D": false, "3D": false,
"diffusionID": "140377765", "diffusionID": "140377765",
"duration": "1800", "duration": "1800",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643", "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771", "URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLLogoBlack": "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" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"
}, },
"diffusions": [ "diffusions": [
{ {
"diffusionDateUTC": 1660815000, "diffusionDateUTC": 1660815000,
"sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", "sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html",
"broadcastId": "140377765", "broadcastId": "140377765",
"name": "BEIN SPORTS 1 HD", "name": "BEIN SPORTS 1 HD",
"epgID": "50115", "epgID": "50115",
"ZapNumber": "191", "ZapNumber": "191",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e" "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e"
} }
] ]
} }
}`) }`)
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content }) parser({ content })
.then(result => { .then(result => {
result = result.map(p => { result = result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-08-18T09:30:00.000Z', start: '2022-08-18T09:30:00.000Z',
stop: '2022-08-18T10:00:00.000Z', stop: '2022-08-18T10:00:00.000Z',
title: 'Rugby - Leinster / La Rochelle', title: 'Rugby - Leinster / La Rochelle',
icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771', icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771',
category: 'Rugby', category: 'Rugby',
description: 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." "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() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
parser({ parser({
content: 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}' '{"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 => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(done) .catch(done)
}) })

View file

@ -1,94 +1,94 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'canalplus-haiti.com', site: 'canalplus-haiti.com',
days: 2, days: 2,
url: function ({ channel, date }) { url: function ({ channel, date }) {
const diff = date.diff(dayjs.utc().startOf('d'), 'd') 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}` return `https://service.canal-overseas.com/ott-frontend/vector/53101/channel/${channel.site_id}/events?filter.day=${diff}`
}, },
async parser({ content }) { async parser({ content }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
for (let item of items) { for (let item of items) {
if (item.title === 'Fin des programmes') return if (item.title === 'Fin des programmes') return
const detail = await loadProgramDetails(item) const detail = await loadProgramDetails(item)
programs.push({ programs.push({
title: item.title, title: item.title,
description: parseDescription(detail), description: parseDescription(detail),
category: parseCategory(detail), category: parseCategory(detail),
icon: parseIcon(item), icon: parseIcon(item),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const html = await axios const html = await axios
.get('https://www.canalplus-haiti.com/guide-tv-ce-soir') .get('https://www.canalplus-haiti.com/guide-tv-ce-soir')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const script = $('body > script:nth-child(2)').html() const script = $('body > script:nth-child(2)').html()
const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null]
const data = JSON.parse(json) const data = JSON.parse(json)
const items = data.tvGuide.channels.byZapNumber const items = data.tvGuide.channels.byZapNumber
return Object.values(items).map(item => { return Object.values(items).map(item => {
return { return {
lang: 'fr', lang: 'fr',
site_id: item.epgID, site_id: item.epgID,
name: item.name name: item.name
} }
}) })
} }
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
if (!item.onClick.URLPage) return {} if (!item.onClick.URLPage) return {}
const url = item.onClick.URLPage const url = item.onClick.URLPage
const data = await axios const data = await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data || {} return data || {}
} }
function parseDescription(detail) { function parseDescription(detail) {
return detail.detail.informations.summary || null return detail.detail.informations.summary || null
} }
function parseCategory(detail) { function parseCategory(detail) {
return detail.detail.informations.subGenre || null return detail.detail.informations.subGenre || null
} }
function parseIcon(item) { function parseIcon(item) {
return item.URLImage || item.URLImageDefault return item.URLImage || item.URLImageDefault
} }
function parseStart(item) { function parseStart(item) {
return dayjs.unix(item.startTime) return dayjs.unix(item.startTime)
} }
function parseStop(item) { function parseStop(item) {
return dayjs.unix(item.endTime) return dayjs.unix(item.endTime)
} }
function parseItems(content) { function parseItems(content) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !data.timeSlices) return [] if (!data || !data.timeSlices) return []
const items = data.timeSlices.reduce((acc, curr) => { const items = data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents) acc = acc.concat(curr.contents)
return acc return acc
}, []) }, [])
return items 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 // [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 // npm run grab -- --site=canalplus-haiti.com
const { parser, url } = require('./canalplus-haiti.com.config.js') const { parser, url } = require('./canalplus-haiti.com.config.js')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const channel = { const channel = {
site_id: '51006', site_id: '51006',
xmltv_id: 'ViaATV.mq' xmltv_id: 'ViaATV.mq'
} }
it('can generate valid url for today', () => { it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d') const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0' 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0'
) )
}) })
it('can generate valid url for tomorrow', () => { it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd') const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1' 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1'
) )
}) })
it('can parse response', done => { it('can parse response', done => {
const content = `{ const content = `{
"timeSlices": [ "timeSlices": [
{ {
"contents": [ "contents": [
{ {
"title": "New Amsterdam - S3 - Ep7", "title": "New Amsterdam - S3 - Ep7",
"subtitle": "Episode 7 - Le mur de la honte", "subtitle": "Episode 7 - Le mur de la honte",
"thirdTitle": "viaATV", "thirdTitle": "viaATV",
"startTime": 1660780500, "startTime": 1660780500,
"endTime": 1660783200, "endTime": 1660783200,
"onClick": { "onClick": {
"displayTemplate": "miniDetail", "displayTemplate": "miniDetail",
"displayName": "New Amsterdam - S3 - Ep7", "displayName": "New Amsterdam - S3 - Ep7",
"URLPage": "https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809", "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" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
}, },
"programID": 187882282, "programID": 187882282,
"diffusionID": "140952809", "diffusionID": "140952809",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e" "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e"
} }
], ],
"timeSlice": "2" "timeSlice": "2"
} }
] ]
}` }`
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') { if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(`{ data: JSON.parse(`{
"currentPage": { "currentPage": {
"displayName": "New Amsterdam - S3 - Ep7", "displayName": "New Amsterdam - S3 - Ep7",
"displayTemplate": "detailPage", "displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
}, },
"detail": { "detail": {
"informations": { "informations": {
"programmeType": "EPG", "programmeType": "EPG",
"isInOffer": false, "isInOffer": false,
"isInOfferOnDevice": false, "isInOfferOnDevice": false,
"isInOfferForD2G": false, "isInOfferForD2G": false,
"availableInVoDOnDevice": false, "availableInVoDOnDevice": false,
"availableInVoDOnG5": false, "availableInVoDOnG5": false,
"availableInD2GOnDevice": false, "availableInD2GOnDevice": false,
"availableInLiveOnDevice": false, "availableInLiveOnDevice": false,
"rediffusions": true, "rediffusions": true,
"canBeRecorded": false, "canBeRecorded": false,
"channelName": "viaATV", "channelName": "viaATV",
"startTime": 1660780500, "startTime": 1660780500,
"endTime": 1660783200, "endTime": 1660783200,
"title": "New Amsterdam - S3 - Ep7", "title": "New Amsterdam - S3 - Ep7",
"subtitle": "Episode 7 - Le mur de la honte", "subtitle": "Episode 7 - Le mur de la honte",
"thirdTitle": "viaATV", "thirdTitle": "viaATV",
"genre": "Séries", "genre": "Séries",
"subGenre": "Série Hôpital", "subGenre": "Série Hôpital",
"editorialTitle": "Séries, Etats-Unis, 2020, 0h45", "editorialTitle": "Séries, Etats-Unis, 2020, 0h45",
"audioLanguage": "VF", "audioLanguage": "VF",
"personnalities": [ "personnalities": [
{ {
"prefix": "De :", "prefix": "De :",
"content": "Darnell Martin" "content": "Darnell Martin"
}, },
{ {
"prefix": "Avec :", "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" "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.", "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.", "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, "programID": 187882282,
"sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html", "sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html",
"labels": { "labels": {
"allocine": false, "allocine": false,
"telerama": false, "telerama": false,
"sensCritique": false "sensCritique": false
}, },
"EpgId": 51006, "EpgId": 51006,
"CSA": 1, "CSA": 1,
"HD": false, "HD": false,
"3D": false, "3D": false,
"diffusionID": "140952809", "diffusionID": "140952809",
"duration": "2700", "duration": "2700",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e", "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLLogoBlack": "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" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations"
}, },
"diffusions": [ "diffusions": [
{ {
"diffusionDateUTC": 1660780500, "diffusionDateUTC": 1660780500,
"sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html", "sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html",
"broadcastId": "140952809", "broadcastId": "140952809",
"name": "viaATV", "name": "viaATV",
"epgID": "51006", "epgID": "51006",
"ZapNumber": "28", "ZapNumber": "28",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce" "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce"
} }
] ]
} }
}`) }`)
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content }) parser({ content })
.then(result => { .then(result => {
result = result.map(p => { result = result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-08-17T23:55:00.000Z', start: '2022-08-17T23:55:00.000Z',
stop: '2022-08-18T00:40:00.000Z', stop: '2022-08-18T00:40:00.000Z',
title: 'New Amsterdam - S3 - Ep7', title: 'New Amsterdam - S3 - Ep7',
icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e', icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e',
category: 'Série Hôpital', category: 'Série Hôpital',
description: 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." "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() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
parser({ parser({
content: 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}' '{"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 => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(done) .catch(done)
}) })

View file

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

View file

@ -1,160 +1,160 @@
// npm run grab -- --site=canalplus-reunion.com // npm run grab -- --site=canalplus-reunion.com
const { parser, url } = require('./canalplus-reunion.com.config.js') const { parser, url } = require('./canalplus-reunion.com.config.js')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const channel = { const channel = {
site_id: '60243', site_id: '60243',
xmltv_id: 'beINSports2France.fr' xmltv_id: 'beINSports2France.fr'
} }
it('can generate valid url for today', () => { it('can generate valid url for today', () => {
const date = dayjs.utc().startOf('d') const date = dayjs.utc().startOf('d')
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0' 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0'
) )
}) })
it('can generate valid url for tomorrow', () => { it('can generate valid url for tomorrow', () => {
const date = dayjs.utc().startOf('d').add(1, 'd') const date = dayjs.utc().startOf('d').add(1, 'd')
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1' 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1'
) )
}) })
it('can parse response', done => { it('can parse response', done => {
const content = `{ const content = `{
"timeSlices": [ "timeSlices": [
{ {
"contents": [ "contents": [
{ {
"title": "Almeria / Real Madrid", "title": "Almeria / Real Madrid",
"subtitle": "Football", "subtitle": "Football",
"thirdTitle": "BEIN SPORTS 2 HD", "thirdTitle": "BEIN SPORTS 2 HD",
"startTime": 1660780800, "startTime": 1660780800,
"endTime": 1660788000, "endTime": 1660788000,
"onClick": { "onClick": {
"displayTemplate": "miniDetail", "displayTemplate": "miniDetail",
"displayName": "Almeria / Real Madrid", "displayName": "Almeria / Real Madrid",
"URLPage": "https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363", "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" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
}, },
"programID": 224523053, "programID": 224523053,
"diffusionID": "140382363", "diffusionID": "140382363",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20" "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20"
} }
], ],
"timeSlice": "4" "timeSlice": "4"
} }
] ]
}` }`
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') { if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(`{ data: JSON.parse(`{
"currentPage": { "currentPage": {
"displayName": "Almeria / Real Madrid", "displayName": "Almeria / Real Madrid",
"displayTemplate": "detailPage", "displayTemplate": "detailPage",
"URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
}, },
"detail": { "detail": {
"informations": { "informations": {
"programmeType": "EPG", "programmeType": "EPG",
"isInOffer": false, "isInOffer": false,
"isInOfferOnDevice": false, "isInOfferOnDevice": false,
"isInOfferForD2G": false, "isInOfferForD2G": false,
"availableInVoDOnDevice": false, "availableInVoDOnDevice": false,
"availableInVoDOnG5": false, "availableInVoDOnG5": false,
"availableInD2GOnDevice": false, "availableInD2GOnDevice": false,
"availableInLiveOnDevice": false, "availableInLiveOnDevice": false,
"rediffusions": true, "rediffusions": true,
"canBeRecorded": false, "canBeRecorded": false,
"channelName": "BEIN SPORTS 2 HD", "channelName": "BEIN SPORTS 2 HD",
"startTime": 1660780800, "startTime": 1660780800,
"endTime": 1660788000, "endTime": 1660788000,
"title": "Almeria / Real Madrid", "title": "Almeria / Real Madrid",
"subtitle": "Football", "subtitle": "Football",
"thirdTitle": "BEIN SPORTS 2 HD", "thirdTitle": "BEIN SPORTS 2 HD",
"genre": "Sport", "genre": "Sport",
"subGenre": "Football", "subGenre": "Football",
"editorialTitle": "Sport, Espagne, 2h00", "editorialTitle": "Sport, Espagne, 2h00",
"audioLanguage": "VF", "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.", "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.", "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, "programID": 224523053,
"sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", "sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html",
"EpgId": 60243, "EpgId": 60243,
"CSA": 1, "CSA": 1,
"HD": false, "HD": false,
"3D": false, "3D": false,
"diffusionID": "140382363", "diffusionID": "140382363",
"duration": "7200", "duration": "7200",
"URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c",
"URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20", "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLLogoBlack": "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" "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations"
}, },
"diffusions": [ "diffusions": [
{ {
"diffusionDateUTC": 1660780800, "diffusionDateUTC": 1660780800,
"sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", "sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html",
"broadcastId": "140382363", "broadcastId": "140382363",
"name": "BEIN SPORTS 2 HD", "name": "BEIN SPORTS 2 HD",
"epgID": "60243", "epgID": "60243",
"ZapNumber": "96", "ZapNumber": "96",
"URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9",
"URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9" "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9"
} }
] ]
} }
}`) }`)
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content }) parser({ content })
.then(result => { .then(result => {
result = result.map(p => { result = result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2022-08-18T00:00:00.000Z', start: '2022-08-18T00:00:00.000Z',
stop: '2022-08-18T02:00:00.000Z', stop: '2022-08-18T02:00:00.000Z',
title: 'Almeria / Real Madrid', title: 'Almeria / Real Madrid',
icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20', icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20',
category: 'Football', category: 'Football',
description: 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." "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() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
parser({ parser({
content: 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}' '{"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 => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(done) .catch(done)
}) })

View file

@ -1,185 +1,185 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
module.exports = { module.exports = {
site: 'canalplus.com', site: 'canalplus.com',
days: 2, days: 2,
url: async function ({ channel, date }) { url: async function ({ channel, date }) {
const [region, site_id] = channel.site_id.split('#') const [region, site_id] = channel.site_id.split('#')
const data = await axios const data = await axios
.get(`https://www.canalplus.com/${region}/programme-tv/`) .get(`https://www.canalplus.com/${region}/programme-tv/`)
.then(r => r.data.toString()) .then(r => r.data.toString())
.catch(err => console.log(err)) .catch(err => console.log(err))
const token = parseToken(data) const token = parseToken(data)
const diff = date.diff(dayjs.utc().startOf('d'), 'd') const diff = date.diff(dayjs.utc().startOf('d'), 'd')
return `https://hodor.canalplus.pro/api/v2/mycanal/channels/${token}/${site_id}/broadcasts/day/${diff}` return `https://hodor.canalplus.pro/api/v2/mycanal/channels/${token}/${site_id}/broadcasts/day/${diff}`
}, },
async parser({ content }) { async parser({ content }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
for (let item of items) { for (let item of items) {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const details = await loadProgramDetails(item) const details = await loadProgramDetails(item)
const info = parseInfo(details) const info = parseInfo(details)
const start = parseStart(item) const start = parseStart(item)
if (prev) prev.stop = start if (prev) prev.stop = start
const stop = start.add(1, 'h') const stop = start.add(1, 'h')
programs.push({ programs.push({
title: item.title, title: item.title,
description: parseDescription(info), description: parseDescription(info),
icon: parseIcon(info), icon: parseIcon(info),
actors: parseCast(info, 'Avec :'), actors: parseCast(info, 'Avec :'),
director: parseCast(info, 'De :'), director: parseCast(info, 'De :'),
writer: parseCast(info, 'Scénario :'), writer: parseCast(info, 'Scénario :'),
composer: parseCast(info, 'Musique :'), composer: parseCast(info, 'Musique :'),
presenter: parseCast(info, 'Présenté par :'), presenter: parseCast(info, 'Présenté par :'),
date: parseDate(info), date: parseDate(info),
rating: parseRating(info), rating: parseRating(info),
start, start,
stop stop
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const endpoints = { const endpoints = {
ad: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ad/all/v2.2/globalchannels.json', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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' yt: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/yt/all/v2.2/globalchannels.json'
} }
let channels = [] let channels = []
for (let [region, url] of Object.entries(endpoints)) { for (let [region, url] of Object.entries(endpoints)) {
const data = await axios const data = await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
data.channels.forEach(channel => { data.channels.forEach(channel => {
const site_id = region === 'fr' ? `#${channel.id}` : `${region}#${channel.id}` const site_id = region === 'fr' ? `#${channel.id}` : `${region}#${channel.id}`
if (channel.name === '.') return if (channel.name === '.') return
channels.push({ channels.push({
lang: 'fr', lang: 'fr',
site_id, site_id,
name: channel.name name: channel.name
}) })
}) })
} }
return channels return channels
} }
} }
function parseToken(data) { function parseToken(data) {
const [, token] = data.match(/"token":"([^"]+)/) || [null, null] const [, token] = data.match(/"token":"([^"]+)/) || [null, null]
return token return token
} }
function parseStart(item) { function parseStart(item) {
return item && item.startTime ? dayjs(item.startTime) : null return item && item.startTime ? dayjs(item.startTime) : null
} }
function parseIcon(info) { function parseIcon(info) {
return info ? info.URLImage : null return info ? info.URLImage : null
} }
function parseDescription(info) { function parseDescription(info) {
return info ? info.summary : null return info ? info.summary : null
} }
function parseInfo(data) { function parseInfo(data) {
if (!data || !data.detail || !data.detail.informations) return null if (!data || !data.detail || !data.detail.informations) return null
return data.detail.informations return data.detail.informations
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
if (!item.onClick || !item.onClick.URLPage) return {} if (!item.onClick || !item.onClick.URLPage) return {}
return await axios return await axios
.get(item.onClick.URLPage) .get(item.onClick.URLPage)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
} }
function parseItems(content) { function parseItems(content) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.timeSlices)) return [] if (!data || !Array.isArray(data.timeSlices)) return []
return data.timeSlices.reduce((acc, curr) => { return data.timeSlices.reduce((acc, curr) => {
acc = acc.concat(curr.contents) acc = acc.concat(curr.contents)
return acc return acc
}, []) }, [])
} }
function parseCast(info, type) { function parseCast(info, type) {
let people = [] let people = []
if (info && info.personnalities) { if (info && info.personnalities) {
const personnalities = info.personnalities.find(i => i.prefix == type) const personnalities = info.personnalities.find(i => i.prefix == type)
if (!personnalities) return people if (!personnalities) return people
for (let person of personnalities.personnalitiesList) { for (let person of personnalities.personnalitiesList) {
people.push(person.title) people.push(person.title)
} }
} }
return people return people
} }
function parseDate(info) { function parseDate(info) {
return info && info.productionYear ? info.productionYear : null return info && info.productionYear ? info.productionYear : null
} }
function parseRating(info) { function parseRating(info) {
if (!info || !info.parentalRatings) return null if (!info || !info.parentalRatings) return null
let rating = info.parentalRatings.find(i => i.authority === 'CSA') let rating = info.parentalRatings.find(i => i.authority === 'CSA')
if (!rating || Array.isArray(rating)) return null if (!rating || Array.isArray(rating)) return null
if (rating.value === '1') return null if (rating.value === '1') return null
if (rating.value === '2') rating.value = '-10' if (rating.value === '2') rating.value = '-10'
if (rating.value === '3') rating.value = '-12' if (rating.value === '3') rating.value = '-12'
if (rating.value === '4') rating.value = '-16' if (rating.value === '4') rating.value = '-16'
if (rating.value === '5') rating.value = '-18' if (rating.value === '5') rating.value = '-18'
return { return {
system: rating.authority, system: rating.authority,
value: rating.value 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 channels:parse -- --config=./sites/canalplus.com/canalplus.com.config.js --output=./sites/canalplus.com/canalplus.com.channels.xml
// npm run grab -- --site=canalplus.com // npm run grab -- --site=canalplus.com
const { parser, url } = require('./canalplus.com.config.js') const { parser, url } = require('./canalplus.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const channel = { const channel = {
site_id: 'bi#198', site_id: 'bi#198',
xmltv_id: 'CanalPlusCinemaFrance.fr' xmltv_id: 'CanalPlusCinemaFrance.fr'
} }
it('can generate valid url for today', done => { it('can generate valid url for today', done => {
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') { if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
const today = dayjs.utc().startOf('d') const today = dayjs.utc().startOf('d')
url({ channel, date: today }) url({ channel, date: today })
.then(result => { .then(result => {
expect(result).toBe( expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0'
) )
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can generate valid url for tomorrow', done => { it('can generate valid url for tomorrow', done => {
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://www.canalplus.com/bi/programme-tv/') { if (url === 'https://www.canalplus.com/bi/programme-tv/') {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html'))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
const tomorrow = dayjs.utc().startOf('d').add(1, 'd') const tomorrow = dayjs.utc().startOf('d').add(1, 'd')
url({ channel, date: tomorrow }) url({ channel, date: tomorrow })
.then(result => { .then(result => {
expect(result).toBe( expect(result).toBe(
'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1'
) )
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can parse response', done => { it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if ( if (
url === 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' '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({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
}) })
} else if ( } else if (
url === url ===
'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' '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({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content }) parser({ content })
.then(result => { .then(result => {
result.map(p => { result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2023-01-12T06:28:00.000Z', start: '2023-01-12T06:28:00.000Z',
stop: '2023-01-12T12:06:00.000Z', stop: '2023-01-12T12:06:00.000Z',
title: 'Le cercle', title: 'Le cercle',
description: 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.", "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', icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573',
presenter: ['Lily Bloom'], presenter: ['Lily Bloom'],
rating: { rating: {
system: 'CSA', system: 'CSA',
value: '-10' value: '-10'
} }
}, },
{ {
start: '2023-01-12T12:06:00.000Z', start: '2023-01-12T12:06:00.000Z',
stop: '2023-01-12T13:06:00.000Z', stop: '2023-01-12T13:06:00.000Z',
title: 'Illusions perdues', title: 'Illusions perdues',
description: 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...", "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', icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485',
director: ['Xavier Giannoli'], director: ['Xavier Giannoli'],
actors: [ actors: [
'Benjamin Voisin', 'Benjamin Voisin',
'Cécile de France', 'Cécile de France',
'Vincent Lacoste', 'Vincent Lacoste',
'Xavier Dolan', 'Xavier Dolan',
'Gérard Depardieu', 'Gérard Depardieu',
'Salomé Dewaels', 'Salomé Dewaels',
'Jeanne Balibar', 'Jeanne Balibar',
'Louis-Do de Lencquesaing', 'Louis-Do de Lencquesaing',
'Alexis Barbosa', 'Alexis Barbosa',
'Jean-François Stévenin', 'Jean-François Stévenin',
'André Marcon', 'André Marcon',
'Marie Cornillon' 'Marie Cornillon'
], ],
writer: ['Xavier Giannoli'], writer: ['Xavier Giannoli'],
rating: { rating: {
system: 'CSA', system: 'CSA',
value: '-10' value: '-10'
} }
} }
]) ])
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const result = await parser({ content }) const result = await parser({ content })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,92 +1,92 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'cgates.lt', site: 'cgates.lt',
days: 2, days: 2,
url: function ({ channel }) { url: function ({ channel }) {
return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/`
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
let programs = [] let programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
const stop = start.add(30, 'm') const stop = start.add(30, 'm')
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
description: parseDescription($item), description: parseDescription($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
let html = await axios let html = await axios
.get('https://www.cgates.lt/televizija/tv-programa-savaitei/') .get('https://www.cgates.lt/televizija/tv-programa-savaitei/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
let $ = cheerio.load(html) let $ = cheerio.load(html)
const items = $('.kanalas_wrap').toArray() const items = $('.kanalas_wrap').toArray()
return items.map(item => { return items.map(item => {
const name = $(item).find('h6').text().trim() const name = $(item).find('h6').text().trim()
const link = $(item).find('a').attr('href') const link = $(item).find('a').attr('href')
const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null]
return { return {
lang: 'lt', lang: 'lt',
site_id, site_id,
name name
} }
}) })
} }
} }
function parseTitle($item) { function parseTitle($item) {
const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim()
return title || $item('td:nth-child(2)').text().trim() return title || $item('td:nth-child(2)').text().trim()
} }
function parseDescription($item) { function parseDescription($item) {
return $item('.vc_toggle_content > p').text().trim() return $item('.vc_toggle_content > p').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const time = $item('.laikas') const time = $item('.laikas')
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius')
} }
function parseItems(content, date) { function parseItems(content, date) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
const section = $( const section = $(
'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div'
) )
.filter(function () { .filter(function () {
return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1
}) })
.first() .first()
return $('.tv_programa tr', section).toArray() 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 channels:parse -- --config=./sites/cgates.lt/cgates.lt.config.js --output=./sites/cgates.lt/cgates.lt.channels.xml
// npm run grab -- --site=cgates.lt // npm run grab -- --site=cgates.lt
const { parser, url } = require('./cgates.lt.config.js') const { parser, url } = require('./cgates.lt.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'lrt-televizija-hd', site_id: 'lrt-televizija-hd',
xmltv_id: 'LRTTV.lt' xmltv_id: 'LRTTV.lt'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => { const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(35) expect(results.length).toBe(35)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-08-29T21:05:00.000Z', start: '2022-08-29T21:05:00.000Z',
stop: '2022-08-29T21:30:00.000Z', stop: '2022-08-29T21:30:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016', title: '31-oji nuovada (District 31), Drama, 2016',
description: 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ą.' '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({ expect(results[34]).toMatchObject({
start: '2022-08-30T20:45:00.000Z', start: '2022-08-30T20:45:00.000Z',
stop: '2022-08-30T21:15:00.000Z', stop: '2022-08-30T21:15:00.000Z',
title: '31-oji nuovada (District 31), Drama, 2016!' title: '31-oji nuovada (District 31), Drama, 2016!'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '' content: ''
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,47 +1,47 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'chaines-tv.orange.fr', site: 'chaines-tv.orange.fr',
days: 2, days: 2,
url({ channel, date }) { 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 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') .add(1, 'd')
.valueOf()}&after=${channel.site_id}&limit=1` .valueOf()}&after=${channel.site_id}&limit=1`
}, },
parser: function ({ content, channel }) { parser: function ({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
const start = parseStart(item) const start = parseStart(item)
const stop = parseStop(item, start) const stop = parseStop(item, start)
programs.push({ programs.push({
title: item.title, title: item.title,
category: item.genreDetailed, category: item.genreDetailed,
description: item.synopsis, description: item.synopsis,
icon: parseIcon(item), icon: parseIcon(item),
start: start.toJSON(), start: start.toJSON(),
stop: stop.toJSON() stop: stop.toJSON()
}) })
}) })
return programs return programs
} }
} }
function parseIcon(item) { function parseIcon(item) {
return item.covers && item.covers.length ? item.covers[0].url : null return item.covers && item.covers.length ? item.covers[0].url : null
} }
function parseStart(item) { function parseStart(item) {
return dayjs.unix(item.diffusionDate) return dayjs.unix(item.diffusionDate)
} }
function parseStop(item, start) { function parseStop(item, start) {
return start.add(item.duration, 's') return start.add(item.duration, 's')
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
return data && data[channel.site_id] ? data[channel.site_id] : [] return data && data[channel.site_id] ? data[channel.site_id] : []
} }

View file

@ -1,48 +1,48 @@
// npm run grab -- --site=chaines-tv.orange.fr // npm run grab -- --site=chaines-tv.orange.fr
const { parser, url } = require('./chaines-tv.orange.fr.config.js') const { parser, url } = require('./chaines-tv.orange.fr.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '192', site_id: '192',
xmltv_id: 'TF1.fr' xmltv_id: 'TF1.fr'
} }
const content = 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"}]}' '{"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', () => { it('can generate valid url', () => {
const result = url({ channel, date }) const result = url({ channel, date })
expect(result).toBe( expect(result).toBe(
'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1' 'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const result = parser({ date, channel, content }) const result = parser({ date, channel, content })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-07T23:35:00.000Z', start: '2021-11-07T23:35:00.000Z',
stop: '2021-11-08T00:20:00.000Z', stop: '2021-11-08T00:20:00.000Z',
title: 'Tête de liste', title: 'Tête de liste',
description: description:
"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.",
category: 'Série Suspense', category: 'Série Suspense',
icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: content:
'{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}' '{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,99 +1,99 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'clickthecity.com', site: 'clickthecity.com',
days: 2, days: 2,
url({ channel }) { url({ channel }) {
return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}`
}, },
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'content-type': 'application/x-www-form-urlencoded' 'content-type': 'application/x-www-form-urlencoded'
}, },
data({ date }) { data({ date }) {
const params = new URLSearchParams() const params = new URLSearchParams()
params.append( params.append(
'optDate', 'optDate',
DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd')
) )
params.append('optTime', '00:00:00') params.append('optTime', '00:00:00')
return params return params
} }
}, },
parser({ content, date }) { parser({ content, date }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
let stop = parseStop($item, date) let stop = parseStop($item, date)
if (!start || !stop) return if (!start || !stop) return
if (start > stop) { if (start > stop) {
stop = stop.plus({ days: 1 }) stop = stop.plus({ days: 1 })
} }
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const html = await axios const html = await axios
.get('https://www.clickthecity.com/tv/channels/') .get('https://www.clickthecity.com/tv/channels/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const items = $('#channels .col').toArray() const items = $('#channels .col').toArray()
return items.map(item => { return items.map(item => {
const name = $(item).find('.card-body').text().trim() const name = $(item).find('.card-body').text().trim()
const url = $(item).find('a').attr('href') const url = $(item).find('a').attr('href')
const [, site_id] = url.match(/netid=(\d+)/) || [null, null] const [, site_id] = url.match(/netid=(\d+)/) || [null, null]
return { return {
site_id, site_id,
name name
} }
}) })
} }
} }
function parseTitle($item) { function parseTitle($item) {
return $item('td > a').text().trim() return $item('td > a').text().trim()
} }
function parseStart($item, date) { function parseStart($item, date) {
const url = $item('td.cPrg > a').attr('href') || '' const url = $item('td.cPrg > a').attr('href') || ''
let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null]
if (!time) return null if (!time) return null
time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}`
return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC()
} }
function parseStop($item, date) { function parseStop($item, date) {
const url = $item('td.cPrg > a').attr('href') || '' const url = $item('td.cPrg > a').attr('href') || ''
let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null]
if (!time) return null if (!time) return null
time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}`
return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC()
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('#tvlistings > tbody > tr') return $('#tvlistings > tbody > tr')
.filter(function () { .filter(function () {
return $(this).find('td.cPrg').length return $(this).find('td.cPrg').length
}) })
.toArray() .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 channels:parse -- --config=./sites/clickthecity.com/clickthecity.com.config.js --output=./sites/clickthecity.com/clickthecity.com.channels.xml
// npm run grab -- --site=clickthecity.com // npm run grab -- --site=clickthecity.com
const { parser, url, request } = require('./clickthecity.com.config.js') const { parser, url, request } = require('./clickthecity.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '5', site_id: '5',
xmltv_id: 'TV5.ph' xmltv_id: 'TV5.ph'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'content-type': 'application/x-www-form-urlencoded' 'content-type': 'application/x-www-form-urlencoded'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const result = request.data({ date }) const result = request.data({ date })
expect(result.get('optDate')).toBe('2023-06-12') expect(result.get('optDate')).toBe('2023-06-12')
expect(result.get('optTime')).toBe('00:00:00') expect(result.get('optTime')).toBe('00:00:00')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }).map(p => { const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(20) expect(results.length).toBe(20)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-06-11T21:00:00.000Z', start: '2023-06-11T21:00:00.000Z',
stop: '2023-06-11T22:00:00.000Z', stop: '2023-06-11T22:00:00.000Z',
title: 'Word Of God' title: 'Word Of God'
}) })
expect(results[19]).toMatchObject({ expect(results[19]).toMatchObject({
start: '2023-06-12T15:30:00.000Z', start: '2023-06-12T15:30:00.000Z',
stop: '2023-06-12T16:00:00.000Z', stop: '2023-06-12T16:00:00.000Z',
title: 'La Suerte De Loli' title: 'La Suerte De Loli'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: content:
'<!DOCTYPE html><html class="html" lang="en-US" prefix="og: https://ogp.me/ns#"><head></head><body></body></html>' '<!DOCTYPE html><html class="html" lang="en-US" prefix="og: https://ogp.me/ns#"><head></head><body></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

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

View file

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

View file

@ -1,74 +1,74 @@
// npm run grab -- --site=comteco.com.bo // npm run grab -- --site=comteco.com.bo
const { parser, url, request } = require('./comteco.com.bo.config.js') const { parser, url, request } = require('./comteco.com.bo.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'ABYA YALA', site_id: 'ABYA YALA',
xmltv_id: 'AbyaYalaTV.bo' xmltv_id: 'AbyaYalaTV.bo'
} }
const content = 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>' '<!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', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe( expect(url({ channel })).toBe(
'https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/ABYA YALA' 'https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/ABYA YALA'
) )
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const result = request.data({ date }) const result = request.data({ date })
expect(result.get('_method')).toBe('POST') expect(result.get('_method')).toBe('POST')
expect(result.get('fechaini')).toBe('25/11/2021') expect(result.get('fechaini')).toBe('25/11/2021')
expect(result.get('fechafin')).toBe('25/11/2021') expect(result.get('fechafin')).toBe('25/11/2021')
}) })
it('can parse response', () => { it('can parse response', () => {
const result = parser({ content, channel, date }).map(p => { const result = parser({ content, channel, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-25T04:00:00.000Z', start: '2021-11-25T04:00:00.000Z',
stop: '2021-11-25T05:00:00.000Z', stop: '2021-11-25T05:00:00.000Z',
title: 'Abya Yala noticias - 3ra edición' title: 'Abya Yala noticias - 3ra edición'
}, },
{ {
start: '2021-11-25T05:00:00.000Z', start: '2021-11-25T05:00:00.000Z',
stop: '2021-11-26T03:00:00.000Z', stop: '2021-11-26T03:00:00.000Z',
title: 'Cierre de emisión' title: 'Cierre de emisión'
}, },
{ {
start: '2021-11-26T03:00:00.000Z', start: '2021-11-26T03:00:00.000Z',
stop: '2021-11-26T03:30:00.000Z', stop: '2021-11-26T03:30:00.000Z',
title: 'Referentes' title: 'Referentes'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>' content: '<!DOCTYPE html><html><head></head><body></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

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

View file

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

View file

@ -1,70 +1,70 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
module.exports = { module.exports = {
site: 'delta.nl', site: 'delta.nl',
days: 2, days: 2,
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date
.add(1, 'd') .add(1, 'd')
.unix()}&includeDetails=true&channels=${channel.site_id}` .unix()}&includeDetails=true&channels=${channel.site_id}`
}, },
async parser({ content, channel }) { async parser({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
for (let item of items) { for (let item of items) {
const details = await loadProgramDetails(item) const details = await loadProgramDetails(item)
programs.push({ programs.push({
title: item.title, title: item.title,
icon: item.images.thumbnail.url, icon: item.images.thumbnail.url,
description: details.description, description: details.description,
start: parseStart(item).toJSON(), start: parseStart(item).toJSON(),
stop: parseStop(item).toJSON() stop: parseStop(item).toJSON()
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const items = await axios const items = await axios
.get('https://clientapi.tv.delta.nl/channels/list') .get('https://clientapi.tv.delta.nl/channels/list')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return items return items
.filter(i => i.type === 'TV') .filter(i => i.type === 'TV')
.map(item => { .map(item => {
return { return {
lang: 'nl', lang: 'nl',
site_id: item['ID'], site_id: item['ID'],
name: item.name name: item.name
} }
}) })
} }
} }
async function loadProgramDetails(item) { async function loadProgramDetails(item) {
if (!item.ID) return {} if (!item.ID) return {}
const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5` const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5`
const data = await axios const data = await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
return data || {} return data || {}
} }
function parseStart(item) { function parseStart(item) {
return dayjs.unix(item.start) return dayjs.unix(item.start)
} }
function parseStop(item) { function parseStop(item) {
return dayjs.unix(item.end) return dayjs.unix(item.end)
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data) return [] if (!data) return []
return data[channel.site_id] || [] 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 channels:parse -- --config=./sites/delta.nl/delta.nl.config.js --output=./sites/delta.nl/delta.nl.channels.xml
// npm run grab -- --site=delta.nl // npm run grab -- --site=delta.nl
const { parser, url } = require('./delta.nl.config.js') const { parser, url } = require('./delta.nl.config.js')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '1', site_id: '1',
xmltv_id: 'NPO1.nl' xmltv_id: 'NPO1.nl'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1' 'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1'
) )
}) })
it('can parse response', done => { it('can parse response', done => {
axios.get.mockImplementation(() => axios.get.mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: JSON.parse( 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}' '{"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 = 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}]}' '{"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 }) parser({ date, channel, content })
.then(result => { .then(result => {
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2021-11-11T23:56:00.000Z', start: '2021-11-11T23:56:00.000Z',
stop: '2021-11-12T00:22:00.000Z', stop: '2021-11-12T00:22:00.000Z',
title: 'NOS Journaal', title: 'NOS Journaal',
description: 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.', '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' icon: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg'
} }
]) ])
done() done()
}) })
.catch(error => { .catch(error => {
done(error) done(error)
}) })
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
parser({ parser({
date, date,
channel, channel,
content: '{"code":500,"message":"Error retrieving guide"}' content: '{"code":500,"message":"Error retrieving guide"}'
}) })
.then(result => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(error => { .catch(error => {
done(error) done(error)
}) })
}) })

View file

@ -1,77 +1,77 @@
const _ = require('lodash') const _ = require('lodash')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
// category list is not complete // category list is not complete
// const categories = { // const categories = {
// '00': 'Diğer', // '00': 'Diğer',
// E0: 'Romantik Komedi', // E0: 'Romantik Komedi',
// E1: 'Aksiyon', // E1: 'Aksiyon',
// E4: 'Macera', // E4: 'Macera',
// E5: 'Dram', // E5: 'Dram',
// E6: 'Fantastik', // E6: 'Fantastik',
// E7: 'Komedi', // E7: 'Komedi',
// E8: 'Korku', // E8: 'Korku',
// EB: 'Polisiye', // EB: 'Polisiye',
// EF: 'Western', // EF: 'Western',
// FA: 'Macera', // FA: 'Macera',
// FB: 'Yarışma', // FB: 'Yarışma',
// FC: 'Eğlence', // FC: 'Eğlence',
// F0: 'Reality-Show', // F0: 'Reality-Show',
// F2: 'Haberler', // F2: 'Haberler',
// F4: 'Belgesel', // F4: 'Belgesel',
// F6: 'Eğitim', // F6: 'Eğitim',
// F7: 'Sanat ve Kültür', // F7: 'Sanat ve Kültür',
// F9: 'Life Style' // F9: 'Life Style'
// } // }
module.exports = { module.exports = {
site: 'digiturk.com.tr', site: 'digiturk.com.tr',
days: 2, days: 2,
url: function ({ date, channel }) { url: function ({ date, channel }) {
return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${ return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${
channel.site_id channel.site_id
}&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false` }&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false`
}, },
request: { request: {
method: 'GET', method: 'GET',
headers: { headers: {
Referer: 'https://www.digiturk.com.tr/' Referer: 'https://www.digiturk.com.tr/'
} }
}, },
parser: function ({ content }) { parser: function ({ content }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: item.PName, title: item.PName,
// description: item.LongDescription, // description: item.LongDescription,
// category: parseCategory(item), // category: parseCategory(item),
start: parseTime(item.PStartTime), start: parseTime(item.PStartTime),
stop: parseTime(item.PEndTime) stop: parseTime(item.PEndTime)
}) })
}) })
programs = _.sortBy(programs, 'start') programs = _.sortBy(programs, 'start')
return programs return programs
} }
} }
function parseTime(time) { function parseTime(time) {
let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', '')) let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', ''))
return dayjs(timestamp) return dayjs(timestamp)
} }
// function parseCategory(item) { // function parseCategory(item) {
// return (item.PGenre) ? categories[item.PGenre] : null // return (item.PGenre) ? categories[item.PGenre] : null
// } // }
function parseItems(content) { function parseItems(content) {
if (!content) return [] if (!content) return []
const data = JSON.parse(content) const data = JSON.parse(content)
return data && data.BChannels && data.BChannels[0].CPrograms ? data.BChannels[0].CPrograms : [] 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 // npm run grab -- --site=digiturk.com.tr
const { parser, url } = require('./digiturk.com.tr.config.js') const { parser, url } = require('./digiturk.com.tr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '14', site_id: '14',
xmltv_id: 'beINMovies2Action.qa' xmltv_id: 'beINMovies2Action.qa'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false' 'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => { const results = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-01-18T20:40:00.000Z', start: '2023-01-18T20:40:00.000Z',
stop: '2023-01-18T22:32:00.000Z', stop: '2023-01-18T22:32:00.000Z',
title: 'PARÇALANMIŞ' title: 'PARÇALANMIŞ'
}) })
expect(results[10]).toMatchObject({ expect(results[10]).toMatchObject({
start: '2023-01-19T05:04:00.000Z', start: '2023-01-19T05:04:00.000Z',
stop: '2023-01-19T06:42:00.000Z', stop: '2023-01-19T06:42:00.000Z',
title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI' title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ content: '' }) const result = parser({ content: '' })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

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

View file

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

View file

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

View file

@ -1,78 +1,78 @@
// npm run grab -- --site=directv.com.uy // npm run grab -- --site=directv.com.uy
const { parser, url, request } = require('./directv.com.uy.config.js') const { parser, url, request } = require('./directv.com.uy.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '184#VTV', site_id: '184#VTV',
xmltv_id: 'VTV.uy' xmltv_id: 'VTV.uy'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming')
}) })
it('can generate valid request method', () => { it('can generate valid request method', () => {
expect(request.method).toBe('POST') expect(request.method).toBe('POST')
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
expect(request.headers).toMatchObject({ expect(request.headers).toMatchObject({
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;'
}) })
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
expect(request.data({ channel, date })).toMatchObject({ expect(request.data({ channel, date })).toMatchObject({
filterParameters: { filterParameters: {
day: 29, day: 29,
time: 0, time: 0,
minute: 0, minute: 0,
month: 8, month: 8,
year: 2022, year: 2022,
offSetValue: 0, offSetValue: 0,
filtersScreenFilters: [''], filtersScreenFilters: [''],
isHd: '', isHd: '',
isChannelDetails: 'Y', isChannelDetails: 'Y',
channelNum: '184', channelNum: '184',
channelName: 'VTV' channelName: 'VTV'
} }
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => { const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-08-29T03:00:00.000Z', start: '2022-08-29T03:00:00.000Z',
stop: '2022-08-29T05: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', title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio',
description: 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).', '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: { rating: {
system: 'MPA', system: 'MPA',
value: 'NR' value: 'NR'
} }
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '', content: '',
channel channel
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,113 +1,113 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'directv.com', site: 'directv.com',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
}, },
headers: { headers: {
'Accept-Language': 'en-US,en;q=0.5', 'Accept-Language': 'en-US,en;q=0.5',
Connection: 'keep-alive' Connection: 'keep-alive'
} }
}, },
url({ date, channel }) { url({ date, channel }) {
const [channelId, childId] = channel.site_id.split('#') const [channelId, childId] = channel.site_id.split('#')
return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}`
}, },
async parser({ content, channel }) { async parser({ content, channel }) {
const programs = [] const programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
for (let item of items) { for (let item of items) {
if (item.programID === '-1') continue if (item.programID === '-1') continue
const detail = await loadProgramDetail(item.programID) const detail = await loadProgramDetail(item.programID)
const start = parseStart(item) const start = parseStart(item)
const stop = start.add(item.duration, 'm') const stop = start.add(item.duration, 'm')
programs.push({ programs.push({
title: item.title, title: item.title,
sub_title: item.episodeTitle, sub_title: item.episodeTitle,
description: parseDescription(detail), description: parseDescription(detail),
rating: parseRating(item), rating: parseRating(item),
date: parseYear(detail), date: parseYear(detail),
category: item.subcategoryList, category: item.subcategoryList,
season: item.seasonNumber, season: item.seasonNumber,
episode: item.episodeNumber, episode: item.episodeNumber,
icon: parseIcon(item), icon: parseIcon(item),
start, start,
stop stop
}) })
} }
return programs return programs
}, },
async channels({ zip }) { async channels({ zip }) {
const html = await axios const html = await axios
.get('https://www.directv.com/guide', { .get('https://www.directv.com/guide', {
headers: { headers: {
cookie: `dtve-prospect-zip=${zip}` cookie: `dtve-prospect-zip=${zip}`
} }
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(html) const $ = cheerio.load(html)
const script = $('#dtvClientData').html() const script = $('#dtvClientData').html()
const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null]
const data = JSON.parse(json) const data = JSON.parse(json)
let items = data.guideData.channels let items = data.guideData.channels
return items.map(item => { return items.map(item => {
return { return {
lang: 'en', lang: 'en',
site_id: item.chNum, site_id: item.chNum,
name: item.chName name: item.chName
} }
}) })
} }
} }
function parseDescription(detail) { function parseDescription(detail) {
return detail ? detail.description : null return detail ? detail.description : null
} }
function parseYear(detail) { function parseYear(detail) {
return detail ? detail.releaseYear : null return detail ? detail.releaseYear : null
} }
function parseRating(item) { function parseRating(item) {
return item.rating return item.rating
? { ? {
system: 'MPA', system: 'MPA',
value: item.rating value: item.rating
} }
: null : null
} }
function parseIcon(item) { function parseIcon(item) {
return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null
} }
function loadProgramDetail(programID) { function loadProgramDetail(programID) {
return axios return axios
.get(`https://www.directv.com/json/program/flip/${programID}`) .get(`https://www.directv.com/json/program/flip/${programID}`)
.then(r => r.data) .then(r => r.data)
.then(d => d.programDetail) .then(d => d.programDetail)
.catch(console.err) .catch(console.err)
} }
function parseStart(item) { function parseStart(item) {
return dayjs.utc(item.airTime) return dayjs.utc(item.airTime)
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data) return [] if (!data) return []
if (!Array.isArray(data.schedule)) return [] if (!Array.isArray(data.schedule)) return []
const [, childId] = channel.site_id.split('#') const [, childId] = channel.site_id.split('#')
const channelData = data.schedule.find(i => i.chId == childId) const channelData = data.schedule.find(i => i.chId == childId)
return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] 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 // 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 // npm run grab -- --site=directv.com
const { parser, url } = require('./directv.com.config.js') const { parser, url } = require('./directv.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '249#249', site_id: '249#249',
xmltv_id: 'ComedyCentralEast.us' xmltv_id: 'ComedyCentralEast.us'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })
expect(result).toBe( expect(result).toBe(
'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249'
) )
}) })
it('can parse response', done => { it('can parse response', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { if (url === 'https://www.directv.com/json/program/flip/MV001173520000') {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json')))
}) })
} else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json')))
}) })
} else { } else {
return Promise.resolve({ data: '' }) return Promise.resolve({ data: '' })
} }
}) })
parser({ content, channel }) parser({ content, channel })
.then(result => { .then(result => {
result = result.map(p => { result = result.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: '2023-01-14T23:00:00.000Z', start: '2023-01-14T23:00:00.000Z',
stop: '2023-01-15T01:00:00.000Z', stop: '2023-01-15T01:00:00.000Z',
title: 'Men in Black II', title: 'Men in Black II',
description: 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.', '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', date: '2002',
icon: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', icon: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg',
category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'],
rating: { rating: {
system: 'MPA', system: 'MPA',
value: 'TV14' value: 'TV14'
} }
}, },
{ {
start: '2023-01-15T06:00:00.000Z', start: '2023-01-15T06:00:00.000Z',
stop: '2023-01-15T06:30:00.000Z', stop: '2023-01-15T06:30:00.000Z',
title: 'South Park', title: 'South Park',
sub_title: 'Goth Kids 3: Dawn of the Posers', sub_title: 'Goth Kids 3: Dawn of the Posers',
description: 'The goth kids are sent to a camp for troubled children.', 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', icon: 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg',
category: ['Series', 'Animation', 'Comedy'], category: ['Series', 'Animation', 'Comedy'],
season: 17, season: 17,
episode: 4, episode: 4,
rating: { rating: {
system: 'MPA', system: 'MPA',
value: 'TVMA' value: 'TVMA'
} }
} }
]) ])
done() done()
}) })
.catch(done) .catch(done)
}) })
it('can handle empty guide', done => { it('can handle empty guide', done => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))
parser({ content, channel }) parser({ content, channel })
.then(result => { .then(result => {
expect(result).toMatchObject([]) expect(result).toMatchObject([])
done() done()
}) })
.catch(done) .catch(done)
}) })

View file

@ -1,145 +1,145 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'dishtv.in', site: 'dishtv.in',
days: 2, days: 2,
url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram',
request: { request: {
method: 'POST', method: 'POST',
data({ channel, date }) { data({ channel, date }) {
return { return {
Channelarr: channel.site_id, Channelarr: channel.site_id,
fromdate: date.format('YYYYMMDDHHmm'), fromdate: date.format('YYYYMMDDHHmm'),
todate: date.add(1, 'd').format('YYYYMMDDHHmm') todate: date.add(1, 'd').format('YYYYMMDDHHmm')
} }
} }
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
let programs = [] let programs = []
const data = parseContent(content) const data = parseContent(content)
const items = parseItems(data) const items = parseItems(data)
items.forEach(item => { items.forEach(item => {
const title = parseTitle(item) const title = parseTitle(item)
const start = parseStart(item, date) const start = parseStart(item, date)
const stop = parseStop(item, start) const stop = parseStop(item, start)
if (title === 'No Information Available') return if (title === 'No Information Available') return
programs.push({ programs.push({
title, title,
start: start.toString(), start: start.toString(),
stop: stop.toString() stop: stop.toString()
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const channelguide = await axios const channelguide = await axios
.get('https://www.dishtv.in/channelguide/') .get('https://www.dishtv.in/channelguide/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $channelguide = cheerio.load(channelguide) const $channelguide = cheerio.load(channelguide)
let ids = [] let ids = []
$channelguide('#MainContent_recordPagging li').each((i, item) => { $channelguide('#MainContent_recordPagging li').each((i, item) => {
const onclick = $channelguide(item).find('a').attr('onclick') const onclick = $channelguide(item).find('a').attr('onclick')
const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null] const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null]
ids = ids.concat(list.split(',')) ids = ids.concat(list.split(','))
}) })
ids = ids.filter(Boolean) ids = ids.filter(Boolean)
const channels = {} const channels = {}
const channelList = await axios const channelList = await axios
.post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', { .post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', {
strChannel: '' strChannel: ''
}) })
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $channelList = cheerio.load(channelList.d) const $channelList = cheerio.load(channelList.d)
$channelList('#tblpackChnl > div').each((i, item) => { $channelList('#tblpackChnl > div').each((i, item) => {
let num = $channelList(item).find('p:nth-child(2)').text().trim() let num = $channelList(item).find('p:nth-child(2)').text().trim()
const name = $channelList(item).find('p').first().text().trim() const name = $channelList(item).find('p').first().text().trim()
if (num === '') return if (num === '') return
channels[parseInt(num)] = { channels[parseInt(num)] = {
name name
} }
}) })
const date = dayjs().add(1, 'd') const date = dayjs().add(1, 'd')
const promises = [] const promises = []
for (let id of ids) { for (let id of ids) {
const promise = axios const promise = axios
.post( .post(
'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram',
{ {
Channelarr: id, Channelarr: id,
fromdate: date.format('YYYYMMDD[0000]'), fromdate: date.format('YYYYMMDD[0000]'),
todate: date.format('YYYYMMDD[2300]') todate: date.format('YYYYMMDD[2300]')
}, },
{ timeout: 5000 } { timeout: 5000 }
) )
.then(r => r.data) .then(r => r.data)
.then(data => { .then(data => {
const $channelGuide = cheerio.load(data.d) const $channelGuide = cheerio.load(data.d)
const num = $channelGuide('.cnl-fav > a > span').text().trim() const num = $channelGuide('.cnl-fav > a > span').text().trim()
if (channels[num]) { if (channels[num]) {
channels[num].site_id = id channels[num].site_id = id
} }
}) })
.catch(console.log) .catch(console.log)
promises.push(promise) promises.push(promise)
} }
await Promise.allSettled(promises) await Promise.allSettled(promises)
return Object.values(channels) return Object.values(channels)
} }
} }
function parseTitle(item) { function parseTitle(item) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
return $('a').text() return $('a').text()
} }
function parseStart(item) { function parseStart(item) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
const onclick = $('i.fa-circle').attr('onclick') const onclick = $('i.fa-circle').attr('onclick')
const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/) const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/)
return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata') return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata')
} }
function parseStop(item, start) { function parseStop(item, start) {
const $ = cheerio.load(item) const $ = cheerio.load(item)
const duration = $('*').data('time') const duration = $('*').data('time')
return start.add(duration, 'm') return start.add(duration, 'm')
} }
function parseContent(content) { function parseContent(content) {
const data = JSON.parse(content) const data = JSON.parse(content)
return data.d return data.d
} }
function parseItems(data) { function parseItems(data) {
const $ = cheerio.load(data) const $ = cheerio.load(data)
return $('.datatime').toArray() 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 channels:parse -- --config=./sites/dishtv.in/dishtv.in.config.js --output=./sites/dishtv.in/dishtv.in.channels.xml
// npm run grab -- --site=dishtv.in // npm run grab -- --site=dishtv.in
const { parser, url, request } = require('./dishtv.in.config.js') const { parser, url, request } = require('./dishtv.in.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' } const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' }
const content = 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"}' '{"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', () => { it('can generate valid url', () => {
expect(url).toBe( expect(url).toBe(
'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram' 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram'
) )
}) })
it('can generate valid request data', () => { it('can generate valid request data', () => {
const result = request.data({ channel, date }) const result = request.data({ channel, date })
expect(result).toMatchObject({ expect(result).toMatchObject({
Channelarr: '10000000075992337', Channelarr: '10000000075992337',
fromdate: '202111050000', fromdate: '202111050000',
todate: '202111060000' todate: '202111060000'
}) })
}) })
it('can parse response', () => { it('can parse response', () => {
const result = parser({ date, channel, content }) const result = parser({ date, channel, content })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
start: 'Thu, 04 Nov 2021 18:30:00 GMT', start: 'Thu, 04 Nov 2021 18:30:00 GMT',
stop: 'Thu, 04 Nov 2021 18:54:00 GMT', stop: 'Thu, 04 Nov 2021 18:54:00 GMT',
title: 'Cynthia Williams - Diwali Look Part 01' title: 'Cynthia Williams - Diwali Look Part 01'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ date, channel, content: '{"d":""}' }) const result = parser({ date, channel, content: '{"d":""}' })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,104 +1,104 @@
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules' const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules'
module.exports = { module.exports = {
site: 'dsmart.com.tr', site: 'dsmart.com.tr',
days: 2, days: 2,
url({ date, channel }) { url({ date, channel }) {
const [page] = channel.site_id.split('#') const [page] = channel.site_id.split('#')
return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}` return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}`
}, },
parser: function ({ content, channel }) { parser: function ({ content, channel }) {
let programs = [] let programs = []
const items = parseItems(content, channel) const items = parseItems(content, channel)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start let start
if (prev) { if (prev) {
start = parseStart(item, prev.stop) start = parseStart(item, prev.stop)
} else { } else {
start = parseStart(item, dayjs.utc(item.day)) start = parseStart(item, dayjs.utc(item.day))
} }
let duration = parseDuration(item) let duration = parseDuration(item)
let stop = start.add(duration, 's') let stop = start.add(duration, 's')
programs.push({ programs.push({
title: item.program_name, title: item.program_name,
category: parseCategory(item), category: parseCategory(item),
description: item.description.trim(), description: item.description.trim(),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const perPage = 1 const perPage = 1
const totalChannels = 210 const totalChannels = 210
const pages = Math.ceil(totalChannels / perPage) const pages = Math.ceil(totalChannels / perPage)
const channels = [] const channels = []
for (let i in Array(pages).fill(0)) { for (let i in Array(pages).fill(0)) {
const page = parseInt(i) + 1 const page = parseInt(i) + 1
const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format( const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format(
'YYYY-MM-DD' 'YYYY-MM-DD'
)}` )}`
let offset = i * perPage let offset = i * perPage
await axios await axios
.get(url) .get(url)
.then(r => r.data) .then(r => r.data)
.then(data => { .then(data => {
offset++ offset++
if (data && data.data && Array.isArray(data.data.channels)) { if (data && data.data && Array.isArray(data.data.channels)) {
data.data.channels.forEach((item, j) => { data.data.channels.forEach((item, j) => {
const index = offset + j const index = offset + j
channels.push({ channels.push({
lang: 'tr', lang: 'tr',
name: item.channel_name, name: item.channel_name,
site_id: index + '#' + item._id site_id: index + '#' + item._id
}) })
}) })
} }
}) })
.catch(err => { .catch(err => {
console.log(err.message) console.log(err.message)
}) })
} }
return channels return channels
} }
} }
function parseCategory(item) { function parseCategory(item) {
return item.genre !== '0' ? item.genre : null return item.genre !== '0' ? item.genre : null
} }
function parseStart(item, date) { function parseStart(item, date) {
const time = dayjs.utc(item.start_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') return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss')
} }
function parseDuration(item) { function parseDuration(item) {
const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/) const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/)
return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss) return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss)
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !data.data || !Array.isArray(data.data.channels)) return null if (!data || !data.data || !Array.isArray(data.data.channels)) return null
const channelData = data.data.channels.find(i => i._id == channelId) const channelData = data.data.channels.find(i => i._id == channelId)
return channelData && Array.isArray(channelData.schedule) ? channelData.schedule : [] 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 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 // npm run grab -- --site=dsmart.com.tr
const { parser, url } = require('./dsmart.com.tr.config.js') const { parser, url } = require('./dsmart.com.tr.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '3#5fe07d7acfef0b1593275751', site_id: '3#5fe07d7acfef0b1593275751',
xmltv_id: 'SinemaTV.tr' xmltv_id: 'SinemaTV.tr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16' 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ channel, content }).map(p => { const results = parser({ channel, content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-01-15T22:00:00.000Z', start: '2023-01-15T22:00:00.000Z',
stop: '2023-01-15T23:45:00.000Z', stop: '2023-01-15T23:45:00.000Z',
title: 'Bizi Ayıran Her Şey', title: 'Bizi Ayıran Her Şey',
category: 'sinema/genel', category: 'sinema/genel',
description: 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.' '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({ expect(results[1]).toMatchObject({
start: '2023-01-15T23:45:00.000Z', start: '2023-01-15T23:45:00.000Z',
stop: '2023-01-16T01:30:00.000Z', stop: '2023-01-16T01:30:00.000Z',
title: 'Pixie', title: 'Pixie',
category: 'sinema/genel', category: 'sinema/genel',
description: 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.' '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({ expect(results[12]).toMatchObject({
start: '2023-01-16T20:30:00.000Z', start: '2023-01-16T20:30:00.000Z',
stop: '2023-01-16T22:30:00.000Z', stop: '2023-01-16T22:30:00.000Z',
title: 'Seberg', title: 'Seberg',
category: 'sinema/genel', category: 'sinema/genel',
description: 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.' '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', () => { it('can handle empty guide', () => {
const results = parser({ const results = parser({
channel, channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,95 +1,95 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
module.exports = { module.exports = {
site: 'entertainment.ie', site: 'entertainment.ie',
days: 2, days: 2,
url: function ({ date, channel }) { url: function ({ date, channel }) {
return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format(
'DD-MM-YYYY' 'DD-MM-YYYY'
)}&time=all-day` )}&time=all-day`
}, },
parser: function ({ content, date }) { parser: function ({ content, date }) {
let programs = [] let programs = []
const items = parseItems(content) const items = parseItems(content)
items.forEach(item => { items.forEach(item => {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
let start = parseStart($item, date) let start = parseStart($item, date)
if (!start) return if (!start) return
if (prev && start < prev.start) { if (prev && start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
} }
const duration = parseDuration($item) const duration = parseDuration($item)
const stop = start.plus({ minutes: duration }) const stop = start.plus({ minutes: duration })
programs.push({ programs.push({
title: parseTitle($item), title: parseTitle($item),
description: parseDescription($item), description: parseDescription($item),
categories: parseCategories($item), categories: parseCategories($item),
icon: parseIcon($item), icon: parseIcon($item),
start, start,
stop stop
}) })
}) })
return programs return programs
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get('https://entertainment.ie/tv/all-channels/') .get('https://entertainment.ie/tv/all-channels/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
const $ = cheerio.load(data) const $ = cheerio.load(data)
let channels = $('.tv-filter-container > tv-filter').attr(':channels') let channels = $('.tv-filter-container > tv-filter').attr(':channels')
channels = JSON.parse(channels) channels = JSON.parse(channels)
return channels.map(c => { return channels.map(c => {
return { return {
site_id: c.slug, site_id: c.slug,
name: c.name name: c.name
} }
}) })
} }
} }
function parseIcon($item) { function parseIcon($item) {
return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img')
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.text-holder h3').text().trim() return $item('.text-holder h3').text().trim()
} }
function parseDescription($item) { function parseDescription($item) {
return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description')
} }
function parseCategories($item) { function parseCategories($item) {
const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres')
return genres ? genres.split(', ') : [] return genres ? genres.split(', ') : []
} }
function parseStart($item, date) { function parseStart($item, date) {
let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time')
let [, time] = d ? d.split(', ') : [null, null] let [, time] = d ? d.split(', ') : [null, null]
return time return time
? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', {
zone: 'UTC' zone: 'UTC'
}).toUTC() }).toUTC()
: null : null
} }
function parseDuration($item) { function parseDuration($item) {
const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration')
return parseInt(duration) return parseInt(duration)
} }
function parseItems(content) { function parseItems(content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
return $('.info-list > li').toArray() 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 channels:parse -- --config=./sites/entertainment.ie/entertainment.ie.config.js --output=./sites/entertainment.ie/entertainment.ie.channels.xml
// npm run grab -- --site=entertainment.ie // npm run grab -- --site=entertainment.ie
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const { parser, url } = require('./entertainment.ie.config.js') const { parser, url } = require('./entertainment.ie.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe( expect(url({ date, channel })).toBe(
'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ date, content }).map(p => { const results = parser({ date, content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results.length).toBe(51) expect(results.length).toBe(51)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2023-06-29T06:00:00.000Z', start: '2023-06-29T06:00:00.000Z',
stop: '2023-06-29T08:00:00.000Z', stop: '2023-06-29T08:00:00.000Z',
title: 'EuroNews', title: 'EuroNews',
description: 'European and international headlines live via satellite', description: 'European and international headlines live via satellite',
icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg',
categories: ['Factual'] categories: ['Factual']
}) })
expect(results[50]).toMatchObject({ expect(results[50]).toMatchObject({
start: '2023-06-30T02:25:00.000Z', start: '2023-06-30T02:25:00.000Z',
stop: '2023-06-30T06:00:00.000Z', stop: '2023-06-30T06:00:00.000Z',
title: 'EuroNews', title: 'EuroNews',
description: 'European and international headlines live via satellite', description: 'European and international headlines live via satellite',
icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg',
categories: ['Factual'] categories: ['Factual']
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html'))
const result = parser({ const result = parser({
date, date,
channel, channel,
content content
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -1,89 +1,89 @@
const axios = require('axios') const axios = require('axios')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel' const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel'
module.exports = { module.exports = {
site: 'epg.i-cable.com', site: 'epg.i-cable.com',
days: 2, days: 2,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1h ttl: 60 * 60 * 1000 // 1h
} }
}, },
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api` return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api`
}, },
parser({ content, channel, date }) { parser({ content, channel, date }) {
const programs = [] const programs = []
const items = parseItems(content, date) const items = parseItems(content, date)
for (let item of items) { for (let item of items) {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
let start = parseStart(item, date) let start = parseStart(item, date)
const stop = start.plus({ minutes: 30 }) const stop = start.plus({ minutes: 30 })
if (prev) { if (prev) {
if (start < prev.start) { if (start < prev.start) {
start = start.plus({ days: 1 }) start = start.plus({ days: 1 })
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
programs.push({ programs.push({
title: parseTitle(item, channel), title: parseTitle(item, channel),
start, start,
stop stop
}) })
} }
return programs return programs
}, },
async channels({ lang }) { async channels({ lang }) {
const data = await axios const data = await axios
.get(`${API_ENDPOINT}/category/0?api=api`) .get(`${API_ENDPOINT}/category/0?api=api`)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
let channels = [] let channels = []
const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`)) const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`))
await Promise.allSettled(promises).then(results => { await Promise.allSettled(promises).then(results => {
results.forEach(r => { results.forEach(r => {
if (r.status === 'fulfilled') { if (r.status === 'fulfilled') {
channels = channels.concat(r.value.data.chs) channels = channels.concat(r.value.data.chs)
} }
}) })
}) })
return channels.map(c => { return channels.map(c => {
let name = lang === 'zh' ? c.channel_name : c.channel_name_en let name = lang === 'zh' ? c.channel_name : c.channel_name_en
name = c.remark_id == 3 ? `${name} [HD]` : name name = c.remark_id == 3 ? `${name} [HD]` : name
return { return {
site_id: c.channel_no, site_id: c.channel_no,
name, name,
lang lang
} }
}) })
} }
} }
function parseTitle(item, channel) { function parseTitle(item, channel) {
return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi
} }
function parseStart(item, date) { function parseStart(item, date) {
let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM' let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM'
return DateTime.fromFormat( return DateTime.fromFormat(
`${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`, `${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`,
'yyyy-MM-dd hh:mm a', 'yyyy-MM-dd hh:mm a',
{ {
zone: 'Asia/Hong_Kong' zone: 'Asia/Hong_Kong'
} }
).toUTC() ).toUTC()
} }
function parseItems(content) { function parseItems(content) {
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data || !Array.isArray(data.epgs)) return [] if (!data || !Array.isArray(data.epgs)) return []
return data.epgs 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 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 // npm run grab -- --site=epg.i-cable.com
const { parser, url } = require('./epg.i-cable.com.config.js') const { parser, url } = require('./epg.i-cable.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '003', site_id: '003',
xmltv_id: 'HOYTV.hk', xmltv_id: 'HOYTV.hk',
lang: 'zh' lang: 'zh'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api' 'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api'
) )
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
let results = parser({ content, channel, date }) let results = parser({ content, channel, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-11-14T22:00:00.000Z', start: '2022-11-14T22:00:00.000Z',
stop: '2022-11-14T23:00:00.000Z', stop: '2022-11-14T23:00:00.000Z',
title: 'Bloomberg 時段' title: 'Bloomberg 時段'
}) })
expect(results[31]).toMatchObject({ expect(results[31]).toMatchObject({
start: '2022-11-15T21:00:00.000Z', start: '2022-11-15T21:00:00.000Z',
stop: '2022-11-15T21:30:00.000Z', stop: '2022-11-15T21:30:00.000Z',
title: 'Bloomberg 時段' title: 'Bloomberg 時段'
}) })
}) })
it('can parse response in English', () => { it('can parse response in English', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const channelEN = { ...channel, lang: 'en' } const channelEN = { ...channel, lang: 'en' }
let results = parser({ content, channel: channelEN, date }) let results = parser({ content, channel: channelEN, date })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2022-11-14T22:00:00.000Z', start: '2022-11-14T22:00:00.000Z',
stop: '2022-11-14T23:00:00.000Z', stop: '2022-11-14T23:00:00.000Z',
title: 'Bloomberg Hour' title: 'Bloomberg Hour'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
const results = parser({ date, channel, content }) const results = parser({ date, channel, content })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

@ -1,52 +1,52 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'firstmedia.com', site: 'firstmedia.com',
days: 1, days: 1,
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${ return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${
channel.site_id channel.site_id
}&start_time=1&end_time=24&need_channels=0` }&start_time=1&end_time=24&need_channels=0`
}, },
parser: function ({ content, channel }) { parser: function ({ content, channel }) {
if (!content || !channel) return [] if (!content || !channel) return []
let programs = [] let programs = []
const items = parseItems(content, channel.site_id) const items = parseItems(content, channel.site_id)
items.forEach(item => { items.forEach(item => {
programs.push({ programs.push({
title: parseTitle(item), title: parseTitle(item),
description: parseDescription(item), description: parseDescription(item),
start: parseStart(item).toISOString(), start: parseStart(item).toISOString(),
stop: parseStop(item).toISOString() stop: parseStop(item).toISOString()
}) })
}) })
return programs return programs
} }
} }
function parseItems(content, channel) { function parseItems(content, channel) {
return JSON.parse(content.trim()).entries[channel] return JSON.parse(content.trim()).entries[channel]
} }
function parseTitle(item) { function parseTitle(item) {
return item.title return item.title
} }
function parseDescription(item) { function parseDescription(item) {
return item.long_description return item.long_description
} }
function parseStart(item) { function parseStart(item) {
return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta')
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') 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 { url, parser } = require('./firstmedia.com.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d') const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d')
const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' } const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( 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' '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', () => { it('can parse response', () => {
const content = 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"}]}}' '{"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 }) const results = parser({ content, channel })
expect(results).toMatchObject([ expect(results).toMatchObject([
{ {
start: '2023-06-13T03:55:00.000Z', start: '2023-06-13T03:55:00.000Z',
stop: '2023-06-13T04:30:00.000Z', stop: '2023-06-13T04:30:00.000Z',
title: 'China Tonight', title: 'China Tonight',
description: 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." "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', () => { it('can handle empty guide', () => {
const results = parser({ content: '' }) const results = parser({ content: '' })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

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

View file

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

View file

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

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