diff --git a/scripts/commands/api/load.ts b/scripts/commands/api/load.ts index 845b6c89..9e0cd0a6 100644 --- a/scripts/commands/api/load.ts +++ b/scripts/commands/api/load.ts @@ -1,18 +1,18 @@ -import { Logger } from '@freearhey/core' -import { ApiClient } from '../../core' - -async function main() { - const logger = new Logger() - const client = new ApiClient({ logger }) - - const requests = [ - client.download('channels.json'), - client.download('countries.json'), - client.download('regions.json'), - client.download('subdivisions.json') - ] - - await Promise.all(requests) -} - -main() +import { Logger } from '@freearhey/core' +import { ApiClient } from '../../core' + +async function main() { + const logger = new Logger() + const client = new ApiClient({ logger }) + + const requests = [ + client.download('channels.json'), + client.download('countries.json'), + client.download('regions.json'), + client.download('subdivisions.json') + ] + + await Promise.all(requests) +} + +main() diff --git a/scripts/commands/channels/editor.ts b/scripts/commands/channels/editor.ts index dada24c9..d94f35a6 100644 --- a/scripts/commands/channels/editor.ts +++ b/scripts/commands/channels/editor.ts @@ -1,180 +1,180 @@ -import { DATA_DIR } from '../../constants' -import { Storage, Collection, Dictionary, Logger } from '@freearhey/core' -import { ChannelsParser, XML, ApiChannel } from '../../core' -import { Channel } from 'epg-grabber' -import { transliterate } from 'transliteration' -import nodeCleanup from 'node-cleanup' -import { program } from 'commander' -import inquirer, { QuestionCollection } from 'inquirer' - -program - .argument('', 'Path to *.channels.xml file to edit') - .option('-c, --country ', 'Default country (ISO 3166 code)', 'US') - .parse(process.argv) - -const filepath = program.args[0] -const programOptions = program.opts() -const defaultCountry = programOptions.country.toLowerCase() -const newLabel = ` [new]` - -let options = new Collection() - -async function main() { - const storage = new Storage() - - if (!(await storage.exists(filepath))) { - throw new Error(`File "${filepath}" does not exists`) - } - - const parser = new ChannelsParser({ storage }) - - const parsedChannels = await parser.parse(filepath) - options = parsedChannels.map((channel: Channel) => { - return { - channel, - delete: false - } - }) - - const dataStorage = new Storage(DATA_DIR) - const channelsContent = await dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) - - const buffer = new Dictionary() - options.forEach(async (option: { channel: Channel; delete: boolean }) => { - const channel = option.channel - if (channel.xmltv_id) { - if (channel.xmltv_id !== '-') { - buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) - } - return - } - let choices = getOptions(channels, channel) - const question: QuestionCollection = { - name: 'option', - message: `Choose an option:`, - type: 'list', - choices, - pageSize: 10 - } - - await inquirer.prompt(question).then(async selected => { - switch (selected.option) { - case 'Overwrite': - const input = await getInput(channel) - channel.xmltv_id = input.xmltv_id - break - case 'Skip': - channel.xmltv_id = '-' - break - default: - const [, xmltv_id] = selected.option - .replace(/ \[.*\]/, '') - .split('|') - .map((i: string) => i.trim().replace(newLabel, '')) - channel.xmltv_id = xmltv_id - break - } - - const found = buffer.has(`${channel.xmltv_id}/${channel.lang}`) - if (found) { - const question: QuestionCollection = { - name: 'option', - message: `"${channel.xmltv_id}" already on the list. Choose an option:`, - type: 'list', - choices: ['Skip', 'Add', 'Delete'], - pageSize: 5 - } - await inquirer.prompt(question).then(async selected => { - switch (selected.option) { - case 'Skip': - channel.xmltv_id = '-' - break - case 'Delete': - option.delete = true - break - default: - break - } - }) - } else { - if (channel.xmltv_id !== '-') { - buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) - } - } - }) - }) -} - -main() - -function save() { - const logger = new Logger() - const storage = new Storage() - - if (!storage.existsSync(filepath)) return - - const channels = options - .filter((option: { channel: Channel; delete: boolean }) => !option.delete) - .map((option: { channel: Channel; delete: boolean }) => option.channel) - - const xml = new XML(channels) - - storage.saveSync(filepath, xml.toString()) - - logger.info(`\nFile '${filepath}' successfully saved`) -} - -nodeCleanup(() => { - save() -}) - -async function getInput(channel: Channel) { - const name = channel.name.trim() - const input = await inquirer.prompt([ - { - name: 'xmltv_id', - message: ' ID:', - type: 'input', - default: generateCode(name, defaultCountry) - } - ]) - - return { name, xmltv_id: input['xmltv_id'] } -} - -function getOptions(channels: Collection, channel: Channel) { - const channelId = generateCode(channel.name, defaultCountry) - const similar = getSimilar(channels, channelId) - - const variants = new Collection() - variants.add(`${channel.name.trim()} | ${channelId}${newLabel}`) - similar.forEach((_channel: ApiChannel) => { - const altNames = _channel.altNames.notEmpty() ? ` (${_channel.altNames.join(',')})` : '' - const closed = _channel.closed ? `[closed:${_channel.closed}]` : `` - const replacedBy = _channel.replacedBy ? `[replaced_by:${_channel.replacedBy}]` : '' - - variants.add(`${_channel.name}${altNames} | ${_channel.id} ${closed}${replacedBy}[api]`) - }) - variants.add(`Overwrite`) - variants.add(`Skip`) - - return variants.all() -} - -function getSimilar(channels: Collection, channelId: string) { - const normChannelId = channelId.split('.')[0].slice(0, 8).toLowerCase() - - return channels.filter((channel: ApiChannel) => - channel.id.split('.')[0].toLowerCase().startsWith(normChannelId) - ) -} - -function generateCode(name: string, country: string) { - const channelId: string = transliterate(name) - .replace(/\+/gi, 'Plus') - .replace(/^\&/gi, 'And') - .replace(/[^a-z\d]+/gi, '') - - return `${channelId}.${country}` -} +import { DATA_DIR } from '../../constants' +import { Storage, Collection, Dictionary, Logger } from '@freearhey/core' +import { ChannelsParser, XML, ApiChannel } from '../../core' +import { Channel } from 'epg-grabber' +import { transliterate } from 'transliteration' +import nodeCleanup from 'node-cleanup' +import { program } from 'commander' +import inquirer, { QuestionCollection } from 'inquirer' + +program + .argument('', 'Path to *.channels.xml file to edit') + .option('-c, --country ', 'Default country (ISO 3166 code)', 'US') + .parse(process.argv) + +const filepath = program.args[0] +const programOptions = program.opts() +const defaultCountry = programOptions.country.toLowerCase() +const newLabel = ' [new]' + +let options = new Collection() + +async function main() { + const storage = new Storage() + + if (!(await storage.exists(filepath))) { + throw new Error(`File "${filepath}" does not exists`) + } + + const parser = new ChannelsParser({ storage }) + + const parsedChannels = await parser.parse(filepath) + options = parsedChannels.map((channel: Channel) => { + return { + channel, + delete: false + } + }) + + const dataStorage = new Storage(DATA_DIR) + const channelsContent = await dataStorage.json('channels.json') + const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + + const buffer = new Dictionary() + options.forEach(async (option: { channel: Channel; delete: boolean }) => { + const channel = option.channel + if (channel.xmltv_id) { + if (channel.xmltv_id !== '-') { + buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) + } + return + } + const choices = getOptions(channels, channel) + const question: QuestionCollection = { + name: 'option', + message: 'Choose an option:', + type: 'list', + choices, + pageSize: 10 + } + + await inquirer.prompt(question).then(async selected => { + switch (selected.option) { + case 'Overwrite': + const input = await getInput(channel) + channel.xmltv_id = input.xmltv_id + break + case 'Skip': + channel.xmltv_id = '-' + break + default: + const [, xmltv_id] = selected.option + .replace(/ \[.*\]/, '') + .split('|') + .map((i: string) => i.trim().replace(newLabel, '')) + channel.xmltv_id = xmltv_id + break + } + + const found = buffer.has(`${channel.xmltv_id}/${channel.lang}`) + if (found) { + const question: QuestionCollection = { + name: 'option', + message: `"${channel.xmltv_id}" already on the list. Choose an option:`, + type: 'list', + choices: ['Skip', 'Add', 'Delete'], + pageSize: 5 + } + await inquirer.prompt(question).then(async selected => { + switch (selected.option) { + case 'Skip': + channel.xmltv_id = '-' + break + case 'Delete': + option.delete = true + break + default: + break + } + }) + } else { + if (channel.xmltv_id !== '-') { + buffer.set(`${channel.xmltv_id}/${channel.lang}`, true) + } + } + }) + }) +} + +main() + +function save() { + const logger = new Logger() + const storage = new Storage() + + if (!storage.existsSync(filepath)) return + + const channels = options + .filter((option: { channel: Channel; delete: boolean }) => !option.delete) + .map((option: { channel: Channel; delete: boolean }) => option.channel) + + const xml = new XML(channels) + + storage.saveSync(filepath, xml.toString()) + + logger.info(`\nFile '${filepath}' successfully saved`) +} + +nodeCleanup(() => { + save() +}) + +async function getInput(channel: Channel) { + const name = channel.name.trim() + const input = await inquirer.prompt([ + { + name: 'xmltv_id', + message: ' ID:', + type: 'input', + default: generateCode(name, defaultCountry) + } + ]) + + return { name, xmltv_id: input['xmltv_id'] } +} + +function getOptions(channels: Collection, channel: Channel) { + const channelId = generateCode(channel.name, defaultCountry) + const similar = getSimilar(channels, channelId) + + const variants = new Collection() + variants.add(`${channel.name.trim()} | ${channelId}${newLabel}`) + similar.forEach((_channel: ApiChannel) => { + const altNames = _channel.altNames.notEmpty() ? ` (${_channel.altNames.join(',')})` : '' + const closed = _channel.closed ? `[closed:${_channel.closed}]` : '' + const replacedBy = _channel.replacedBy ? `[replaced_by:${_channel.replacedBy}]` : '' + + variants.add(`${_channel.name}${altNames} | ${_channel.id} ${closed}${replacedBy}[api]`) + }) + variants.add('Overwrite') + variants.add('Skip') + + return variants.all() +} + +function getSimilar(channels: Collection, channelId: string) { + const normChannelId = channelId.split('.')[0].slice(0, 8).toLowerCase() + + return channels.filter((channel: ApiChannel) => + channel.id.split('.')[0].toLowerCase().startsWith(normChannelId) + ) +} + +function generateCode(name: string, country: string) { + const channelId: string = transliterate(name) + .replace(/\+/gi, 'Plus') + .replace(/^&/gi, 'And') + .replace(/[^a-z\d]+/gi, '') + + return `${channelId}.${country}` +} diff --git a/scripts/commands/channels/lint.ts b/scripts/commands/channels/lint.ts index e486f9d1..37c6b646 100644 --- a/scripts/commands/channels/lint.ts +++ b/scripts/commands/channels/lint.ts @@ -1,78 +1,78 @@ -import chalk from 'chalk' -import libxml, { ValidationError } from 'libxmljs2' -import { program } from 'commander' -import { Logger, Storage, File } from '@freearhey/core' - -const xsd = ` - - - - - - - - - - - - - - - - - -` - -program - .option( - '-c, --channels ', - 'Path to channels.xml file to validate', - 'sites/**/*.channels.xml' - ) - .parse(process.argv) - -const options = program.opts() - -async function main() { - const logger = new Logger() - const storage = new Storage() - - logger.info('options:') - logger.tree(options) - - let errors: ValidationError[] = [] - - let files: string[] = await storage.list(options.channels) - for (const filepath of files) { - const file = new File(filepath) - if (file.extension() !== 'xml') continue - - const xml = await storage.load(filepath) - - let localErrors: ValidationError[] = [] - - const xsdDoc = libxml.parseXml(xsd) - const doc = libxml.parseXml(xml) - - if (!doc.validate(xsdDoc)) { - localErrors = doc.validationErrors - } - - if (localErrors.length) { - console.log(`\n${chalk.underline(filepath)}`) - localErrors.forEach((error: ValidationError) => { - const position = `${error.line}:${error.column}` - console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`) - }) - - errors = errors.concat(localErrors) - } - } - - if (errors.length) { - console.log(chalk.red(`\n${errors.length} error(s)`)) - process.exit(1) - } -} - -main() +import chalk from 'chalk' +import libxml, { ValidationError } from 'libxmljs2' +import { program } from 'commander' +import { Logger, Storage, File } from '@freearhey/core' + +const xsd = ` + + + + + + + + + + + + + + + + + +` + +program + .option( + '-c, --channels ', + 'Path to channels.xml file to validate', + 'sites/**/*.channels.xml' + ) + .parse(process.argv) + +const options = program.opts() + +async function main() { + const logger = new Logger() + const storage = new Storage() + + logger.info('options:') + logger.tree(options) + + let errors: ValidationError[] = [] + + const files: string[] = await storage.list(options.channels) + for (const filepath of files) { + const file = new File(filepath) + if (file.extension() !== 'xml') continue + + const xml = await storage.load(filepath) + + let localErrors: ValidationError[] = [] + + const xsdDoc = libxml.parseXml(xsd) + const doc = libxml.parseXml(xml) + + if (!doc.validate(xsdDoc)) { + localErrors = doc.validationErrors + } + + if (localErrors.length) { + console.log(`\n${chalk.underline(filepath)}`) + localErrors.forEach((error: ValidationError) => { + const position = `${error.line}:${error.column}` + console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`) + }) + + errors = errors.concat(localErrors) + } + } + + if (errors.length) { + console.log(chalk.red(`\n${errors.length} error(s)`)) + process.exit(1) + } +} + +main() diff --git a/scripts/commands/channels/parse.ts b/scripts/commands/channels/parse.ts index 31f80125..c9185652 100644 --- a/scripts/commands/channels/parse.ts +++ b/scripts/commands/channels/parse.ts @@ -1,81 +1,85 @@ -import { Logger, File, Collection, Storage } from '@freearhey/core' -import { ChannelsParser, XML } from '../../core' -import { Channel } from 'epg-grabber' -import { Command, OptionValues } from 'commander' -import path from 'path' - -const program = new Command() -program - .requiredOption('-c, --config ', 'Config file') - .option('-s, --set [args...]', 'Set custom arguments') - .option('-o, --output ', 'Output file') - .option('--clean', 'Delete the previous *.channels.xml if exists') - .parse(process.argv) - -type ParseOptions = { - config: string - set?: string - output?: string - clean?: boolean -} - -const options: ParseOptions = program.opts() - -async function main() { - const storage = new Storage() - const parser = new ChannelsParser({ storage }) - const logger = new Logger() - const file = new File(options.config) - const dir = file.dirname() - const config = require(path.resolve(options.config)) - const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` - - let channels = new Collection() - if (!options.clean && (await storage.exists(outputFilepath))) { - channels = await parser.parse(outputFilepath) - } - - const args: { - [key: string]: any - } = {} - - if (Array.isArray(options.set)) { - options.set.forEach((arg: string) => { - const [key, value] = arg.split(':') - args[key] = value - }) - } - - let parsedChannels = config.channels(args) - if (isPromise(parsedChannels)) { - parsedChannels = await parsedChannels - } - parsedChannels = parsedChannels.map((channel: Channel) => { - channel.site = config.site - - return channel - }) - - channels = channels - .mergeBy( - new Collection(parsedChannels), - (channel: Channel) => channel.site_id.toString() + channel.lang - ) - .orderBy([ - (channel: Channel) => channel.lang, - (channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'), - (channel: Channel) => channel.site_id - ]) - - const xml = new XML(channels) - - await storage.save(outputFilepath, xml.toString()) - - logger.info(`File '${outputFilepath}' successfully saved`) -} - -main() - -function isPromise(promise: any) { - return !!promise && typeof promise.then === 'function' -} +import { Logger, File, Collection, Storage } from '@freearhey/core' +import { ChannelsParser, XML } from '../../core' +import { Channel } from 'epg-grabber' +import { Command } from 'commander' +import path from 'path' + +const program = new Command() +program + .requiredOption('-c, --config ', 'Config file') + .option('-s, --set [args...]', 'Set custom arguments') + .option('-o, --output ', 'Output file') + .option('--clean', 'Delete the previous *.channels.xml if exists') + .parse(process.argv) + +type ParseOptions = { + config: string + set?: string + output?: string + clean?: boolean +} + +const options: ParseOptions = program.opts() + +async function main() { + const storage = new Storage() + const parser = new ChannelsParser({ storage }) + const logger = new Logger() + const file = new File(options.config) + const dir = file.dirname() + const config = require(path.resolve(options.config)) + const outputFilepath = options.output || `${dir}/${config.site}.channels.xml` + + let channels = new Collection() + if (!options.clean && (await storage.exists(outputFilepath))) { + channels = await parser.parse(outputFilepath) + } + + const args: { + [key: string]: string + } = {} + + if (Array.isArray(options.set)) { + options.set.forEach((arg: string) => { + const [key, value] = arg.split(':') + args[key] = value + }) + } + + let parsedChannels = config.channels(args) + if (isPromise(parsedChannels)) { + parsedChannels = await parsedChannels + } + parsedChannels = parsedChannels.map((channel: Channel) => { + channel.site = config.site + + return channel + }) + + channels = channels + .mergeBy( + new Collection(parsedChannels), + (channel: Channel) => channel.site_id.toString() + channel.lang + ) + .orderBy([ + (channel: Channel) => channel.lang, + (channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '_'), + (channel: Channel) => channel.site_id + ]) + + const xml = new XML(channels) + + await storage.save(outputFilepath, xml.toString()) + + logger.info(`File '${outputFilepath}' successfully saved`) +} + +main() + +function isPromise(promise: object[] | Promise) { + return ( + !!promise && + typeof promise === 'object' && + typeof (promise as Promise).then === 'function' + ) +} diff --git a/scripts/commands/channels/validate.ts b/scripts/commands/channels/validate.ts index acda8863..2a057aac 100644 --- a/scripts/commands/channels/validate.ts +++ b/scripts/commands/channels/validate.ts @@ -1,95 +1,95 @@ -import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core' -import { ChannelsParser, ApiChannel } from '../../core' -import { program } from 'commander' -import chalk from 'chalk' -import langs from 'langs' -import { DATA_DIR } from '../../constants' -import { Channel } from 'epg-grabber' - -program - .option( - '-c, --channels ', - 'Path to channels.xml file to validate', - 'sites/**/*.channels.xml' - ) - .parse(process.argv) - -const options = program.opts() - -type ValidationError = { - type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang' - name: string - lang?: string - xmltv_id?: string - site_id?: string - logo?: string -} - -async function main() { - const logger = new Logger() - - logger.info('options:') - logger.tree(options) - - const parser = new ChannelsParser({ storage: new Storage() }) - - const dataStorage = new Storage(DATA_DIR) - const channelsContent = await dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) - - let totalFiles = 0 - let totalErrors = 0 - const storage = new Storage() - let files: string[] = await storage.list(options.channels) - for (const filepath of files) { - const file = new File(filepath) - if (file.extension() !== 'xml') continue - - const parsedChannels = await parser.parse(filepath) - - const bufferById = new Dictionary() - const bufferBySiteId = new Dictionary() - const errors: ValidationError[] = [] - parsedChannels.forEach((channel: Channel) => { - const bufferId: string = `${channel.xmltv_id}:${channel.lang}` - if (bufferById.missing(bufferId)) { - bufferById.set(bufferId, true) - } else { - errors.push({ type: 'duplicate', ...channel }) - totalErrors++ - } - - const bufferSiteId: string = `${channel.site_id}:${channel.lang}` - if (bufferBySiteId.missing(bufferSiteId)) { - bufferBySiteId.set(bufferSiteId, true) - } else { - errors.push({ type: 'duplicate', ...channel }) - totalErrors++ - } - - if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) { - errors.push({ type: 'wrong_xmltv_id', ...channel }) - totalErrors++ - } - - if (!langs.where('1', channel.lang)) { - errors.push({ type: 'wrong_lang', ...channel }) - totalErrors++ - } - }) - - if (errors.length) { - console.log(chalk.underline(filepath)) - console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name']) - console.log() - totalFiles++ - } - } - - if (totalErrors > 0) { - console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`)) - process.exit(1) - } -} - -main() +import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core' +import { ChannelsParser, ApiChannel } from '../../core' +import { program } from 'commander' +import chalk from 'chalk' +import langs from 'langs' +import { DATA_DIR } from '../../constants' +import { Channel } from 'epg-grabber' + +program + .option( + '-c, --channels ', + 'Path to channels.xml file to validate', + 'sites/**/*.channels.xml' + ) + .parse(process.argv) + +const options = program.opts() + +type ValidationError = { + type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang' + name: string + lang?: string + xmltv_id?: string + site_id?: string + logo?: string +} + +async function main() { + const logger = new Logger() + + logger.info('options:') + logger.tree(options) + + const parser = new ChannelsParser({ storage: new Storage() }) + + const dataStorage = new Storage(DATA_DIR) + const channelsContent = await dataStorage.json('channels.json') + const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + + let totalFiles = 0 + let totalErrors = 0 + const storage = new Storage() + const files: string[] = await storage.list(options.channels) + for (const filepath of files) { + const file = new File(filepath) + if (file.extension() !== 'xml') continue + + const parsedChannels = await parser.parse(filepath) + + const bufferById = new Dictionary() + const bufferBySiteId = new Dictionary() + const errors: ValidationError[] = [] + parsedChannels.forEach((channel: Channel) => { + const bufferId: string = `${channel.xmltv_id}:${channel.lang}` + if (bufferById.missing(bufferId)) { + bufferById.set(bufferId, true) + } else { + errors.push({ type: 'duplicate', ...channel }) + totalErrors++ + } + + const bufferSiteId: string = `${channel.site_id}:${channel.lang}` + if (bufferBySiteId.missing(bufferSiteId)) { + bufferBySiteId.set(bufferSiteId, true) + } else { + errors.push({ type: 'duplicate', ...channel }) + totalErrors++ + } + + if (channels.missing((_channel: ApiChannel) => _channel.id === channel.xmltv_id)) { + errors.push({ type: 'wrong_xmltv_id', ...channel }) + totalErrors++ + } + + if (!langs.where('1', channel.lang)) { + errors.push({ type: 'wrong_lang', ...channel }) + totalErrors++ + } + }) + + if (errors.length) { + console.log(chalk.underline(filepath)) + console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name']) + console.log() + totalFiles++ + } + } + + if (totalErrors > 0) { + console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`)) + process.exit(1) + } +} + +main() diff --git a/scripts/commands/epg/grab.ts b/scripts/commands/epg/grab.ts index 9741c8d7..cc0d03f2 100644 --- a/scripts/commands/epg/grab.ts +++ b/scripts/commands/epg/grab.ts @@ -1,117 +1,117 @@ -import { Logger, Timer, Storage, Collection } from '@freearhey/core' -import { program } from 'commander' -import { CronJob } from 'cron' -import { QueueCreator, Job, ChannelsParser } from '../../core' -import { Channel } from 'epg-grabber' -import path from 'path' -import { SITES_DIR } from '../../constants' - -program - .option('-s, --site ', 'Name of the site to parse') - .option( - '-c, --channels ', - 'Path to *.channels.xml file (required if the "--site" attribute is not specified)' - ) - .option('-o, --output ', 'Path to output file', 'guide.xml') - .option('-l, --lang ', 'Filter channels by language (ISO 639-2 code)') - .option('-t, --timeout ', 'Override the default timeout for each request') - .option( - '--days ', - 'Override the number of days for which the program will be loaded (defaults to the value from the site config)', - value => parseInt(value) - ) - .option( - '--maxConnections ', - 'Limit on the number of concurrent requests', - value => parseInt(value), - 1 - ) - .option('--cron ', 'Schedule a script run (example: "0 0 * * *")') - .option('--gzip', 'Create a compressed version of the guide as well', false) - .parse(process.argv) - -export type GrabOptions = { - site?: string - channels?: string - output: string - gzip: boolean - maxConnections: number - timeout?: string - lang?: string - days?: number - cron?: string -} - -const options: GrabOptions = program.opts() - -async function main() { - if (!options.site && !options.channels) - throw new Error('One of the arguments must be presented: `--site` or `--channels`') - - const logger = new Logger() - - logger.start('staring...') - - logger.info('config:') - logger.tree(options) - - logger.info(`loading channels...`) - const storage = new Storage() - const parser = new ChannelsParser({ storage }) - - let files: string[] = [] - if (options.site) { - let pattern = path.join(SITES_DIR, options.site, '*.channels.xml') - pattern = pattern.replace(/\\/g, '/') - files = await storage.list(pattern) - } else if (options.channels) { - files = await storage.list(options.channels) - } - - let parsedChannels = new Collection() - for (let filepath of files) { - parsedChannels = parsedChannels.concat(await parser.parse(filepath)) - } - if (options.lang) { - parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang) - } - logger.info(` found ${parsedChannels.count()} channels`) - - logger.info('creating queue...') - const queueCreator = new QueueCreator({ - parsedChannels, - logger, - options - }) - const queue = await queueCreator.create() - logger.info(` added ${queue.size()} items`) - - const job = new Job({ - queue, - logger, - options - }) - - let runIndex = 1 - if (options.cron) { - const cronJob = new CronJob(options.cron, async () => { - logger.info(`run #${runIndex}:`) - const timer = new Timer() - timer.start() - await job.run() - runIndex++ - logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) - }) - cronJob.start() - } else { - logger.info(`run #${runIndex}:`) - const timer = new Timer() - timer.start() - await job.run() - logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) - } - - logger.info('finished') -} - -main() +import { Logger, Timer, Storage, Collection } from '@freearhey/core' +import { program } from 'commander' +import { CronJob } from 'cron' +import { QueueCreator, Job, ChannelsParser } from '../../core' +import { Channel } from 'epg-grabber' +import path from 'path' +import { SITES_DIR } from '../../constants' + +program + .option('-s, --site ', 'Name of the site to parse') + .option( + '-c, --channels ', + 'Path to *.channels.xml file (required if the "--site" attribute is not specified)' + ) + .option('-o, --output ', 'Path to output file', 'guide.xml') + .option('-l, --lang ', 'Filter channels by language (ISO 639-2 code)') + .option('-t, --timeout ', 'Override the default timeout for each request') + .option( + '--days ', + 'Override the number of days for which the program will be loaded (defaults to the value from the site config)', + value => parseInt(value) + ) + .option( + '--maxConnections ', + 'Limit on the number of concurrent requests', + value => parseInt(value), + 1 + ) + .option('--cron ', 'Schedule a script run (example: "0 0 * * *")') + .option('--gzip', 'Create a compressed version of the guide as well', false) + .parse(process.argv) + +export type GrabOptions = { + site?: string + channels?: string + output: string + gzip: boolean + maxConnections: number + timeout?: string + lang?: string + days?: number + cron?: string +} + +const options: GrabOptions = program.opts() + +async function main() { + if (!options.site && !options.channels) + throw new Error('One of the arguments must be presented: `--site` or `--channels`') + + const logger = new Logger() + + logger.start('staring...') + + logger.info('config:') + logger.tree(options) + + logger.info('loading channels...') + const storage = new Storage() + const parser = new ChannelsParser({ storage }) + + let files: string[] = [] + if (options.site) { + let pattern = path.join(SITES_DIR, options.site, '*.channels.xml') + pattern = pattern.replace(/\\/g, '/') + files = await storage.list(pattern) + } else if (options.channels) { + files = await storage.list(options.channels) + } + + let parsedChannels = new Collection() + for (const filepath of files) { + parsedChannels = parsedChannels.concat(await parser.parse(filepath)) + } + if (options.lang) { + parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang) + } + logger.info(` found ${parsedChannels.count()} channels`) + + logger.info('creating queue...') + const queueCreator = new QueueCreator({ + parsedChannels, + logger, + options + }) + const queue = await queueCreator.create() + logger.info(` added ${queue.size()} items`) + + const job = new Job({ + queue, + logger, + options + }) + + let runIndex = 1 + if (options.cron) { + const cronJob = new CronJob(options.cron, async () => { + logger.info(`run #${runIndex}:`) + const timer = new Timer() + timer.start() + await job.run() + runIndex++ + logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) + }) + cronJob.start() + } else { + logger.info(`run #${runIndex}:`) + const timer = new Timer() + timer.start() + await job.run() + logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`) + } + + logger.info('finished') +} + +main() diff --git a/scripts/constants.ts b/scripts/constants.ts index 7b6c00d0..25231ce0 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -1,4 +1,4 @@ -export const SITES_DIR = process.env.SITES_DIR || './sites' -export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' -export const DATA_DIR = process.env.DATA_DIR || './temp/data' -export const CURR_DATE = process.env.CURR_DATE || new Date().toISOString() +export const SITES_DIR = process.env.SITES_DIR || './sites' +export const GUIDES_DIR = process.env.GUIDES_DIR || './guides' +export const DATA_DIR = process.env.DATA_DIR || './temp/data' +export const CURR_DATE = process.env.CURR_DATE || new Date().toISOString() diff --git a/scripts/core/apiChannel.ts b/scripts/core/apiChannel.ts index 3ddd1a91..fd1fcd69 100644 --- a/scripts/core/apiChannel.ts +++ b/scripts/core/apiChannel.ts @@ -1,79 +1,79 @@ -import { Collection } from '@freearhey/core' - -type ApiChannelProps = { - id: string - name: string - alt_names: string[] - network: string - owners: string[] - country: string - subdivision: string - city: string - broadcast_area: string[] - languages: string[] - categories: string[] - is_nsfw: boolean - launched: string - closed: string - replaced_by: string - website: string - logo: string -} - -export class ApiChannel { - id: string - name: string - altNames: Collection - network: string - owners: Collection - country: string - subdivision: string - city: string - broadcastArea: Collection - languages: Collection - categories: Collection - isNSFW: boolean - launched: string - closed: string - replacedBy: string - website: string - logo: string - - constructor({ - id, - name, - alt_names, - network, - owners, - country, - subdivision, - city, - broadcast_area, - languages, - categories, - is_nsfw, - launched, - closed, - replaced_by, - website, - logo - }: ApiChannelProps) { - this.id = id - this.name = name - this.altNames = new Collection(alt_names) - this.network = network - this.owners = new Collection(owners) - this.country = country - this.subdivision = subdivision - this.city = city - this.broadcastArea = new Collection(broadcast_area) - this.languages = new Collection(languages) - this.categories = new Collection(categories) - this.isNSFW = is_nsfw - this.launched = launched - this.closed = closed - this.replacedBy = replaced_by - this.website = website - this.logo = logo - } -} +import { Collection } from '@freearhey/core' + +type ApiChannelProps = { + id: string + name: string + alt_names: string[] + network: string + owners: string[] + country: string + subdivision: string + city: string + broadcast_area: string[] + languages: string[] + categories: string[] + is_nsfw: boolean + launched: string + closed: string + replaced_by: string + website: string + logo: string +} + +export class ApiChannel { + id: string + name: string + altNames: Collection + network: string + owners: Collection + country: string + subdivision: string + city: string + broadcastArea: Collection + languages: Collection + categories: Collection + isNSFW: boolean + launched: string + closed: string + replacedBy: string + website: string + logo: string + + constructor({ + id, + name, + alt_names, + network, + owners, + country, + subdivision, + city, + broadcast_area, + languages, + categories, + is_nsfw, + launched, + closed, + replaced_by, + website, + logo + }: ApiChannelProps) { + this.id = id + this.name = name + this.altNames = new Collection(alt_names) + this.network = network + this.owners = new Collection(owners) + this.country = country + this.subdivision = subdivision + this.city = city + this.broadcastArea = new Collection(broadcast_area) + this.languages = new Collection(languages) + this.categories = new Collection(categories) + this.isNSFW = is_nsfw + this.launched = launched + this.closed = closed + this.replacedBy = replaced_by + this.website = website + this.logo = logo + } +} diff --git a/scripts/core/apiClient.ts b/scripts/core/apiClient.ts index 66fa28a8..2db0f3d0 100644 --- a/scripts/core/apiClient.ts +++ b/scripts/core/apiClient.ts @@ -1,59 +1,59 @@ -import { Logger, Storage } from '@freearhey/core' -import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios' -import cliProgress, { MultiBar } from 'cli-progress' -import numeral from 'numeral' - -export class ApiClient { - progressBar: MultiBar - client: AxiosInstance - storage: Storage - logger: Logger - - constructor({ logger }: { logger: Logger }) { - this.logger = logger - this.client = axios.create({ - responseType: 'stream' - }) - this.storage = new Storage() - this.progressBar = new cliProgress.MultiBar({ - stopOnComplete: true, - hideCursor: true, - forceRedraw: true, - barsize: 36, - format(options, params, payload) { - const filename = payload.filename.padEnd(18, ' ') - const barsize = options.barsize || 40 - const percent = (params.progress * 100).toFixed(2) - const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' - const total = numeral(params.total).format('0.0 b') - const completeSize = Math.round(params.progress * barsize) - const incompleteSize = barsize - completeSize - const bar = - options.barCompleteString && options.barIncompleteString - ? options.barCompleteString.substr(0, completeSize) + - options.barGlue + - options.barIncompleteString.substr(0, incompleteSize) - : '-'.repeat(barsize) - - return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` - } - }) - } - - async download(filename: string) { - const stream = await this.storage.createStream(`/temp/data/${filename}`) - - const bar = this.progressBar.create(0, 0, { filename }) - - this.client - .get(`https://iptv-org.github.io/api/${filename}`, { - onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) { - if (total) bar.setTotal(total) - bar.update(loaded, { speed: rate }) - } - }) - .then((response: AxiosResponse) => { - response.data.pipe(stream) - }) - } -} +import { Logger, Storage } from '@freearhey/core' +import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios' +import cliProgress, { MultiBar } from 'cli-progress' +import numeral from 'numeral' + +export class ApiClient { + progressBar: MultiBar + client: AxiosInstance + storage: Storage + logger: Logger + + constructor({ logger }: { logger: Logger }) { + this.logger = logger + this.client = axios.create({ + responseType: 'stream' + }) + this.storage = new Storage() + this.progressBar = new cliProgress.MultiBar({ + stopOnComplete: true, + hideCursor: true, + forceRedraw: true, + barsize: 36, + format(options, params, payload) { + const filename = payload.filename.padEnd(18, ' ') + const barsize = options.barsize || 40 + const percent = (params.progress * 100).toFixed(2) + const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' + const total = numeral(params.total).format('0.0 b') + const completeSize = Math.round(params.progress * barsize) + const incompleteSize = barsize - completeSize + const bar = + options.barCompleteString && options.barIncompleteString + ? options.barCompleteString.substr(0, completeSize) + + options.barGlue + + options.barIncompleteString.substr(0, incompleteSize) + : '-'.repeat(barsize) + + return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` + } + }) + } + + async download(filename: string) { + const stream = await this.storage.createStream(`/temp/data/${filename}`) + + const bar = this.progressBar.create(0, 0, { filename }) + + this.client + .get(`https://iptv-org.github.io/api/${filename}`, { + onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) { + if (total) bar.setTotal(total) + bar.update(loaded, { speed: rate }) + } + }) + .then((response: AxiosResponse) => { + response.data.pipe(stream) + }) + } +} diff --git a/scripts/core/channelsParser.ts b/scripts/core/channelsParser.ts index 990422b4..d4630506 100644 --- a/scripts/core/channelsParser.ts +++ b/scripts/core/channelsParser.ts @@ -1,24 +1,24 @@ -import { parseChannels } from 'epg-grabber' -import { Storage, Collection } from '@freearhey/core' - -type ChannelsParserProps = { - storage: Storage -} - -export class ChannelsParser { - storage: Storage - - constructor({ storage }: ChannelsParserProps) { - this.storage = storage - } - - async parse(filepath: string) { - let parsedChannels = new Collection() - - const content = await this.storage.load(filepath) - const channels = parseChannels(content) - parsedChannels = parsedChannels.concat(new Collection(channels)) - - return parsedChannels - } -} +import { parseChannels } from 'epg-grabber' +import { Storage, Collection } from '@freearhey/core' + +type ChannelsParserProps = { + storage: Storage +} + +export class ChannelsParser { + storage: Storage + + constructor({ storage }: ChannelsParserProps) { + this.storage = storage + } + + async parse(filepath: string) { + let parsedChannels = new Collection() + + const content = await this.storage.load(filepath) + const channels = parseChannels(content) + parsedChannels = parsedChannels.concat(new Collection(channels)) + + return parsedChannels + } +} diff --git a/scripts/core/configLoader.ts b/scripts/core/configLoader.ts index 2cff65d8..31c29c76 100644 --- a/scripts/core/configLoader.ts +++ b/scripts/core/configLoader.ts @@ -1,21 +1,21 @@ -import { SiteConfig } from 'epg-grabber' -import _ from 'lodash' -import { pathToFileURL } from 'url' - -export class ConfigLoader { - async load(filepath: string): Promise { - const fileUrl = pathToFileURL(filepath).toString() - const config = (await import(fileUrl)).default - - return _.merge( - { - delay: 0, - maxConnections: 1, - request: { - timeout: 30000 - } - }, - config - ) - } -} +import { SiteConfig } from 'epg-grabber' +import _ from 'lodash' +import { pathToFileURL } from 'url' + +export class ConfigLoader { + async load(filepath: string): Promise { + const fileUrl = pathToFileURL(filepath).toString() + const config = (await import(fileUrl)).default + + return _.merge( + { + delay: 0, + maxConnections: 1, + request: { + timeout: 30000 + } + }, + config + ) + } +} diff --git a/scripts/core/date.js b/scripts/core/date.js index 4dff5199..5515a1c4 100644 --- a/scripts/core/date.js +++ b/scripts/core/date.js @@ -1,13 +1,13 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = {} - -date.getUTC = function (d = null) { - if (typeof d === 'string') return dayjs.utc(d).startOf('d') - - return dayjs.utc().startOf('d') -} - -module.exports = date +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = {} + +date.getUTC = function (d = null) { + if (typeof d === 'string') return dayjs.utc(d).startOf('d') + + return dayjs.utc().startOf('d') +} + +module.exports = date diff --git a/scripts/core/grabber.ts b/scripts/core/grabber.ts index 783e3f14..c91d66ff 100644 --- a/scripts/core/grabber.ts +++ b/scripts/core/grabber.ts @@ -1,75 +1,75 @@ -import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber' -import { Logger, Collection } from '@freearhey/core' -import { Queue } from './' -import { GrabOptions } from '../commands/epg/grab' -import { TaskQueue, PromisyClass } from 'cwait' - -type GrabberProps = { - logger: Logger - queue: Queue - options: GrabOptions -} - -export class Grabber { - logger: Logger - queue: Queue - options: GrabOptions - - constructor({ logger, queue, options }: GrabberProps) { - this.logger = logger - this.queue = queue - this.options = options - } - - async grab(): Promise<{ channels: Collection; programs: Collection }> { - const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections) - - const total = this.queue.size() - - const channels = new Collection() - let programs = new Collection() - let i = 1 - - await Promise.all( - this.queue.items().map( - taskQueue.wrap( - async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => { - const { channel, config, date } = queueItem - - channels.add(channel) - - if (this.options.timeout !== undefined) { - const timeout = parseInt(this.options.timeout) - config.request = { ...config.request, ...{ timeout } } - } - - const grabber = - process.env.NODE_ENV === 'test' ? new EPGGrabberMock(config) : new EPGGrabber(config) - const _programs = await grabber.grab( - channel, - date, - (data: GrabCallbackData, error: Error | null) => { - const { programs, date } = data - - this.logger.info( - ` [${i}/${total}] ${channel.site} (${channel.lang}) - ${ - channel.xmltv_id - } - ${date.format('MMM D, YYYY')} (${programs.length} programs)` - ) - if (i < total) i++ - - if (error) { - this.logger.info(` ERR: ${error.message}`) - } - } - ) - - programs = programs.concat(new Collection(_programs)) - } - ) - ) - ) - - return { channels, programs } - } -} +import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber' +import { Logger, Collection } from '@freearhey/core' +import { Queue } from './' +import { GrabOptions } from '../commands/epg/grab' +import { TaskQueue, PromisyClass } from 'cwait' + +type GrabberProps = { + logger: Logger + queue: Queue + options: GrabOptions +} + +export class Grabber { + logger: Logger + queue: Queue + options: GrabOptions + + constructor({ logger, queue, options }: GrabberProps) { + this.logger = logger + this.queue = queue + this.options = options + } + + async grab(): Promise<{ channels: Collection; programs: Collection }> { + const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections) + + const total = this.queue.size() + + const channels = new Collection() + let programs = new Collection() + let i = 1 + + await Promise.all( + this.queue.items().map( + taskQueue.wrap( + async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => { + const { channel, config, date } = queueItem + + channels.add(channel) + + if (this.options.timeout !== undefined) { + const timeout = parseInt(this.options.timeout) + config.request = { ...config.request, ...{ timeout } } + } + + const grabber = + process.env.NODE_ENV === 'test' ? new EPGGrabberMock(config) : new EPGGrabber(config) + const _programs = await grabber.grab( + channel, + date, + (data: GrabCallbackData, error: Error | null) => { + const { programs, date } = data + + this.logger.info( + ` [${i}/${total}] ${channel.site} (${channel.lang}) - ${ + channel.xmltv_id + } - ${date.format('MMM D, YYYY')} (${programs.length} programs)` + ) + if (i < total) i++ + + if (error) { + this.logger.info(` ERR: ${error.message}`) + } + } + ) + + programs = programs.concat(new Collection(_programs)) + } + ) + ) + ) + + return { channels, programs } + } +} diff --git a/scripts/core/guide.ts b/scripts/core/guide.ts index 2b73f05e..3b6804c9 100644 --- a/scripts/core/guide.ts +++ b/scripts/core/guide.ts @@ -1,55 +1,55 @@ -import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core' -import { Channel } from 'epg-grabber' -import { XMLTV } from '../core' -import { CURR_DATE } from '../constants' - -type GuideProps = { - channels: Collection - programs: Collection - logger: Logger - filepath: string - gzip: boolean -} - -export class Guide { - channels: Collection - programs: Collection - logger: Logger - storage: Storage - filepath: string - gzip: boolean - - constructor({ channels, programs, logger, filepath, gzip }: GuideProps) { - this.channels = channels - this.programs = programs - this.logger = logger - this.storage = new Storage() - this.filepath = filepath - this.gzip = gzip || false - } - - async save() { - const channels = this.channels.uniqBy( - (channel: Channel) => `${channel.xmltv_id}:${channel.site}` - ) - const programs = this.programs - - const xmltv = new XMLTV({ - channels, - programs, - date: new DateTime(CURR_DATE, { zone: 'UTC' }) - }) - - const xmlFilepath = this.filepath - this.logger.info(` saving to "${xmlFilepath}"...`) - await this.storage.save(xmlFilepath, xmltv.toString()) - - if (this.gzip) { - const zip = new Zip() - const compressed = await zip.compress(xmltv.toString()) - const gzFilepath = `${this.filepath}.gz` - this.logger.info(` saving to "${gzFilepath}"...`) - await this.storage.save(gzFilepath, compressed) - } - } -} +import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core' +import { Channel } from 'epg-grabber' +import { XMLTV } from '../core' +import { CURR_DATE } from '../constants' + +type GuideProps = { + channels: Collection + programs: Collection + logger: Logger + filepath: string + gzip: boolean +} + +export class Guide { + channels: Collection + programs: Collection + logger: Logger + storage: Storage + filepath: string + gzip: boolean + + constructor({ channels, programs, logger, filepath, gzip }: GuideProps) { + this.channels = channels + this.programs = programs + this.logger = logger + this.storage = new Storage() + this.filepath = filepath + this.gzip = gzip || false + } + + async save() { + const channels = this.channels.uniqBy( + (channel: Channel) => `${channel.xmltv_id}:${channel.site}` + ) + const programs = this.programs + + const xmltv = new XMLTV({ + channels, + programs, + date: new DateTime(CURR_DATE, { zone: 'UTC' }) + }) + + const xmlFilepath = this.filepath + this.logger.info(` saving to "${xmlFilepath}"...`) + await this.storage.save(xmlFilepath, xmltv.toString()) + + if (this.gzip) { + const zip = new Zip() + const compressed = await zip.compress(xmltv.toString()) + const gzFilepath = `${this.filepath}.gz` + this.logger.info(` saving to "${gzFilepath}"...`) + await this.storage.save(gzFilepath, compressed) + } + } +} diff --git a/scripts/core/guideManager.ts b/scripts/core/guideManager.ts index 22b80552..78640a32 100644 --- a/scripts/core/guideManager.ts +++ b/scripts/core/guideManager.ts @@ -1,61 +1,61 @@ -import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core' -import { OptionValues } from 'commander' -import { Channel, Program } from 'epg-grabber' -import { Guide } from '.' - -type GuideManagerProps = { - options: OptionValues - logger: Logger - channels: Collection - programs: Collection -} - -export class GuideManager { - options: OptionValues - storage: Storage - logger: Logger - channels: Collection - programs: Collection - - constructor({ channels, programs, logger, options }: GuideManagerProps) { - this.options = options - this.logger = logger - this.channels = channels - this.programs = programs - this.storage = new Storage() - } - - async createGuides() { - const pathTemplate = new StringTemplate(this.options.output) - - const groupedChannels = this.channels - .orderBy([(channel: Channel) => channel.xmltv_id]) - .uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`) - .groupBy((channel: Channel) => { - return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' }) - }) - - const groupedPrograms = this.programs - .orderBy([(program: Program) => program.channel, (program: Program) => program.start]) - .groupBy((program: Program) => { - const lang = - program.titles && program.titles.length && program.titles[0].lang - ? program.titles[0].lang - : 'en' - - return pathTemplate.format({ lang, site: program.site || '' }) - }) - - for (const groupKey of groupedPrograms.keys()) { - const guide = new Guide({ - filepath: groupKey, - gzip: this.options.gzip, - channels: new Collection(groupedChannels.get(groupKey)), - programs: new Collection(groupedPrograms.get(groupKey)), - logger: this.logger - }) - - await guide.save() - } - } -} +import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core' +import { OptionValues } from 'commander' +import { Channel, Program } from 'epg-grabber' +import { Guide } from '.' + +type GuideManagerProps = { + options: OptionValues + logger: Logger + channels: Collection + programs: Collection +} + +export class GuideManager { + options: OptionValues + storage: Storage + logger: Logger + channels: Collection + programs: Collection + + constructor({ channels, programs, logger, options }: GuideManagerProps) { + this.options = options + this.logger = logger + this.channels = channels + this.programs = programs + this.storage = new Storage() + } + + async createGuides() { + const pathTemplate = new StringTemplate(this.options.output) + + const groupedChannels = this.channels + .orderBy([(channel: Channel) => channel.xmltv_id]) + .uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`) + .groupBy((channel: Channel) => { + return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' }) + }) + + const groupedPrograms = this.programs + .orderBy([(program: Program) => program.channel, (program: Program) => program.start]) + .groupBy((program: Program) => { + const lang = + program.titles && program.titles.length && program.titles[0].lang + ? program.titles[0].lang + : 'en' + + return pathTemplate.format({ lang, site: program.site || '' }) + }) + + for (const groupKey of groupedPrograms.keys()) { + const guide = new Guide({ + filepath: groupKey, + gzip: this.options.gzip, + channels: new Collection(groupedChannels.get(groupKey)), + programs: new Collection(groupedPrograms.get(groupKey)), + logger: this.logger + }) + + await guide.save() + } + } +} diff --git a/scripts/core/index.ts b/scripts/core/index.ts index 0af8e636..fca249a5 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -1,12 +1,12 @@ -export * from './xml' -export * from './channelsParser' -export * from './xmltv' -export * from './configLoader' -export * from './grabber' -export * from './job' -export * from './queue' -export * from './guideManager' -export * from './guide' -export * from './apiChannel' -export * from './apiClient' -export * from './queueCreator' +export * from './xml' +export * from './channelsParser' +export * from './xmltv' +export * from './configLoader' +export * from './grabber' +export * from './job' +export * from './queue' +export * from './guideManager' +export * from './guide' +export * from './apiChannel' +export * from './apiClient' +export * from './queueCreator' diff --git a/scripts/core/job.ts b/scripts/core/job.ts index d23b59f6..9dbf7308 100644 --- a/scripts/core/job.ts +++ b/scripts/core/job.ts @@ -1,34 +1,34 @@ -import { Logger } from '@freearhey/core' -import { Queue, Grabber, GuideManager } from '.' -import { GrabOptions } from '../commands/epg/grab' - -type JobProps = { - options: GrabOptions - logger: Logger - queue: Queue -} - -export class Job { - options: GrabOptions - logger: Logger - grabber: Grabber - - constructor({ queue, logger, options }: JobProps) { - this.options = options - this.logger = logger - this.grabber = new Grabber({ logger, queue, options }) - } - - async run() { - const { channels, programs } = await this.grabber.grab() - - const manager = new GuideManager({ - channels, - programs, - options: this.options, - logger: this.logger - }) - - await manager.createGuides() - } -} +import { Logger } from '@freearhey/core' +import { Queue, Grabber, GuideManager } from '.' +import { GrabOptions } from '../commands/epg/grab' + +type JobProps = { + options: GrabOptions + logger: Logger + queue: Queue +} + +export class Job { + options: GrabOptions + logger: Logger + grabber: Grabber + + constructor({ queue, logger, options }: JobProps) { + this.options = options + this.logger = logger + this.grabber = new Grabber({ logger, queue, options }) + } + + async run() { + const { channels, programs } = await this.grabber.grab() + + const manager = new GuideManager({ + channels, + programs, + options: this.options, + logger: this.logger + }) + + await manager.createGuides() + } +} diff --git a/scripts/core/queue.ts b/scripts/core/queue.ts index 28af9d88..06ef3961 100644 --- a/scripts/core/queue.ts +++ b/scripts/core/queue.ts @@ -1,45 +1,45 @@ -import { Dictionary } from '@freearhey/core' -import { SiteConfig, Channel } from 'epg-grabber' - -export type QueueItem = { - channel: Channel - date: string - config: SiteConfig - error: string | null -} - -export class Queue { - _data: Dictionary - - constructor() { - this._data = new Dictionary() - } - - missing(key: string): boolean { - return this._data.missing(key) - } - - add( - key: string, - { channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig } - ) { - this._data.set(key, { - channel, - date, - config, - error: null - }) - } - - size(): number { - return Object.values(this._data.data()).length - } - - items(): QueueItem[] { - return Object.values(this._data.data()) as QueueItem[] - } - - isEmpty(): boolean { - return this.size() === 0 - } -} +import { Dictionary } from '@freearhey/core' +import { SiteConfig, Channel } from 'epg-grabber' + +export type QueueItem = { + channel: Channel + date: string + config: SiteConfig + error: string | null +} + +export class Queue { + _data: Dictionary + + constructor() { + this._data = new Dictionary() + } + + missing(key: string): boolean { + return this._data.missing(key) + } + + add( + key: string, + { channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig } + ) { + this._data.set(key, { + channel, + date, + config, + error: null + }) + } + + size(): number { + return Object.values(this._data.data()).length + } + + items(): QueueItem[] { + return Object.values(this._data.data()) as QueueItem[] + } + + isEmpty(): boolean { + return this.size() === 0 + } +} diff --git a/scripts/core/queueCreator.ts b/scripts/core/queueCreator.ts index b23a8f4a..ed0e5435 100644 --- a/scripts/core/queueCreator.ts +++ b/scripts/core/queueCreator.ts @@ -1,71 +1,71 @@ -import { Storage, Collection, DateTime, Logger } from '@freearhey/core' -import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './' -import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants' -import { SiteConfig } from 'epg-grabber' -import path from 'path' -import { GrabOptions } from '../commands/epg/grab' - -type QueueCreatorProps = { - logger: Logger - options: GrabOptions - parsedChannels: Collection -} - -export class QueueCreator { - configLoader: ConfigLoader - logger: Logger - sitesStorage: Storage - dataStorage: Storage - parser: ChannelsParser - parsedChannels: Collection - options: GrabOptions - date: DateTime - - constructor({ parsedChannels, logger, options }: QueueCreatorProps) { - this.parsedChannels = parsedChannels - this.logger = logger - this.sitesStorage = new Storage() - this.dataStorage = new Storage(DATA_DIR) - this.parser = new ChannelsParser({ storage: new Storage() }) - this.date = new DateTime(CURR_DATE) - this.options = options - this.configLoader = new ConfigLoader() - } - - async create(): Promise { - const channelsContent = await this.dataStorage.json('channels.json') - const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) - - const queue = new Queue() - for (const channel of this.parsedChannels.all()) { - if (!channel.site || !channel.xmltv_id) continue - if (this.options.lang && channel.lang !== this.options.lang) continue - - const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) - const config: SiteConfig = await this.configLoader.load(configPath) - - const found: ApiChannel = channels.first( - (_channel: ApiChannel) => _channel.id === channel.xmltv_id - ) - if (found) { - channel.logo = found.logo - } - - const days = this.options.days || config.days || 1 - const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd')) - dates.forEach((date: DateTime) => { - const dateString = date.toJSON() - const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` - if (queue.missing(key)) { - queue.add(key, { - channel, - date: dateString, - config - }) - } - }) - } - - return queue - } -} +import { Storage, Collection, DateTime, Logger } from '@freearhey/core' +import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './' +import { SITES_DIR, DATA_DIR, CURR_DATE } from '../constants' +import { SiteConfig } from 'epg-grabber' +import path from 'path' +import { GrabOptions } from '../commands/epg/grab' + +type QueueCreatorProps = { + logger: Logger + options: GrabOptions + parsedChannels: Collection +} + +export class QueueCreator { + configLoader: ConfigLoader + logger: Logger + sitesStorage: Storage + dataStorage: Storage + parser: ChannelsParser + parsedChannels: Collection + options: GrabOptions + date: DateTime + + constructor({ parsedChannels, logger, options }: QueueCreatorProps) { + this.parsedChannels = parsedChannels + this.logger = logger + this.sitesStorage = new Storage() + this.dataStorage = new Storage(DATA_DIR) + this.parser = new ChannelsParser({ storage: new Storage() }) + this.date = new DateTime(CURR_DATE) + this.options = options + this.configLoader = new ConfigLoader() + } + + async create(): Promise { + const channelsContent = await this.dataStorage.json('channels.json') + const channels = new Collection(channelsContent).map(data => new ApiChannel(data)) + + const queue = new Queue() + for (const channel of this.parsedChannels.all()) { + if (!channel.site || !channel.xmltv_id) continue + if (this.options.lang && channel.lang !== this.options.lang) continue + + const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`) + const config: SiteConfig = await this.configLoader.load(configPath) + + const found: ApiChannel = channels.first( + (_channel: ApiChannel) => _channel.id === channel.xmltv_id + ) + if (found) { + channel.logo = found.logo + } + + const days = this.options.days || config.days || 1 + const dates = Array.from({ length: days }, (_, day) => this.date.add(day, 'd')) + dates.forEach((date: DateTime) => { + const dateString = date.toJSON() + const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}` + if (queue.missing(key)) { + queue.add(key, { + channel, + date: dateString, + config + }) + } + }) + } + + return queue + } +} diff --git a/scripts/core/xml.ts b/scripts/core/xml.ts index 8c644fa2..fd42f993 100644 --- a/scripts/core/xml.ts +++ b/scripts/core/xml.ts @@ -1,56 +1,56 @@ -import { Collection } from '@freearhey/core' -import { Channel } from 'epg-grabber' - -export class XML { - items: Collection - - constructor(items: Collection) { - this.items = items - } - - toString() { - let output = '\r\n\r\n' - - this.items.forEach((channel: Channel) => { - const logo = channel.logo ? ` logo="${channel.logo}"` : '' - const xmltv_id = channel.xmltv_id || '' - const lang = channel.lang || '' - const site_id = channel.site_id || '' - output += ` ${escapeString(channel.name)}\r\n` - }) - - output += '\r\n' - - return output - } -} - -function escapeString(value: string, defaultValue: string = '') { - if (!value) return defaultValue - - const regex = new RegExp( - '((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' + - 'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' + - 'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' + - '|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' + - 'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' + - '[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' + - 'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' + - '(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))', - 'g' - ) - - value = String(value || '').replace(regex, '') - - return value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\n|\r/g, ' ') - .replace(/ +/g, ' ') - .trim() -} +import { Collection } from '@freearhey/core' +import { Channel } from 'epg-grabber' + +export class XML { + items: Collection + + constructor(items: Collection) { + this.items = items + } + + toString() { + let output = '\r\n\r\n' + + this.items.forEach((channel: Channel) => { + const logo = channel.logo ? ` logo="${channel.logo}"` : '' + const xmltv_id = channel.xmltv_id || '' + const lang = channel.lang || '' + const site_id = channel.site_id || '' + output += ` ${escapeString(channel.name)}\r\n` + }) + + output += '\r\n' + + return output + } +} + +function escapeString(value: string, defaultValue: string = '') { + if (!value) return defaultValue + + const regex = new RegExp( + '((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' + + 'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' + + 'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' + + '|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' + + 'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' + + '[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' + + 'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' + + '(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))', + 'g' + ) + + value = String(value || '').replace(regex, '') + + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\n|\r/g, ' ') + .replace(/ +/g, ' ') + .trim() +} diff --git a/scripts/core/xmltv.ts b/scripts/core/xmltv.ts index eebd5424..5603c4e7 100644 --- a/scripts/core/xmltv.ts +++ b/scripts/core/xmltv.ts @@ -1,28 +1,28 @@ -import { DateTime, Collection } from '@freearhey/core' -import { generateXMLTV } from 'epg-grabber' - -type XMLTVProps = { - channels: Collection - programs: Collection - date: DateTime -} - -export class XMLTV { - channels: Collection - programs: Collection - date: DateTime - - constructor({ channels, programs, date }: XMLTVProps) { - this.channels = channels - this.programs = programs - this.date = date - } - - toString() { - return generateXMLTV({ - channels: this.channels.all(), - programs: this.programs.all(), - date: this.date.toJSON() - }) - } -} +import { DateTime, Collection } from '@freearhey/core' +import { generateXMLTV } from 'epg-grabber' + +type XMLTVProps = { + channels: Collection + programs: Collection + date: DateTime +} + +export class XMLTV { + channels: Collection + programs: Collection + date: DateTime + + constructor({ channels, programs, date }: XMLTVProps) { + this.channels = channels + this.programs = programs + this.date = date + } + + toString() { + return generateXMLTV({ + channels: this.channels.all(), + programs: this.programs.all(), + date: this.date.toJSON() + }) + } +} diff --git a/scripts/types/langs.d.ts b/scripts/types/langs.d.ts index 60fb498a..74921c68 100644 --- a/scripts/types/langs.d.ts +++ b/scripts/types/langs.d.ts @@ -1 +1 @@ -declare module 'langs' +declare module 'langs' diff --git a/sites/9tv.co.il/9tv.co.il.config.js b/sites/9tv.co.il/9tv.co.il.config.js index fcc1c312..aafd4bb6 100644 --- a/sites/9tv.co.il/9tv.co.il.config.js +++ b/sites/9tv.co.il/9tv.co.il.config.js @@ -1,69 +1,69 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: '9tv.co.il', - days: 2, - url: function ({ date }) { - return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( - 'DD/MM/YYYY 00:00:00' - )}` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const start = parseStart($item, date) - if (prev) prev.stop = start - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - icon: parseIcon($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - let time = $item('a > div.guide_list_time').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem') -} - -function parseIcon($item) { - const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( - 'background-image' - ) - if (!backgroundImage) return null - const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] - - return relativePath ? `https://www.9tv.co.il${relativePath}` : null -} - -function parseDescription($item) { - return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() -} - -function parseTitle($item) { - return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('li').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: '9tv.co.il', + days: 2, + url: function ({ date }) { + return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format( + 'DD/MM/YYYY 00:00:00' + )}` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const start = parseStart($item, date) + if (prev) prev.stop = start + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + icon: parseIcon($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + let time = $item('a > div.guide_list_time').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem') +} + +function parseIcon($item) { + const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css( + 'background-image' + ) + if (!backgroundImage) return null + const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null] + + return relativePath ? `https://www.9tv.co.il${relativePath}` : null +} + +function parseDescription($item) { + return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim() +} + +function parseTitle($item) { + return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('li').toArray() +} diff --git a/sites/9tv.co.il/9tv.co.il.test.js b/sites/9tv.co.il/9tv.co.il.test.js index fce629ca..90be4841 100644 --- a/sites/9tv.co.il/9tv.co.il.test.js +++ b/sites/9tv.co.il/9tv.co.il.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=9tv.co.il - -const { parser, url } = require('./9tv.co.il.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'Channel9.il' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00' - ) -}) - -it('can parse response', () => { - const content = - '
  • 06:30

    Слепая

    Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. 
  • 09:10

    Орел и решка. Морской сезон

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

    Слепая

    Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. 
  • 09:10

    Орел и решка. Морской сезон

    Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.
  • ' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-06T04:30:00.000Z', + stop: '2022-03-06T07:10:00.000Z', + title: 'Слепая', + icon: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg', + description: + 'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.' + }, + { + start: '2022-03-06T07:10:00.000Z', + stop: '2022-03-06T08:10:00.000Z', + icon: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg', + title: 'Орел и решка. Морской сезон', + description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/abc.net.au/abc.net.au.config.js b/sites/abc.net.au/abc.net.au.config.js index b3212833..26388f47 100644 --- a/sites/abc.net.au/abc.net.au.config.js +++ b/sites/abc.net.au/abc.net.au.config.js @@ -1,77 +1,77 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'abc.net.au', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://epg.abctv.net.au/processed/Sydney_${date.format('YYYY-MM-DD')}.json` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.episode_title, - category: item.genres, - description: item.description, - season: parseSeason(item), - episode: parseEpisode(item), - rating: parseRating(item), - icon: parseIcon(item), - start: parseTime(item.start_time), - stop: parseTime(item.end_time) - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data.schedule)) return [] - - const channelData = data.schedule.find(i => i.channel == channel.site_id) - return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] - } catch (err) { - return [] - } -} - -function parseSeason(item) { - return item.series_num || null -} -function parseEpisode(item) { - return item.episode_num || null -} -function parseTime(time) { - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney') -} -function parseIcon(item) { - return item.image_file - ? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}` - : null -} -function parseRating(item) { - return item.rating - ? { - system: 'ACB', - value: item.rating - } - : null -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'abc.net.au', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://epg.abctv.net.au/processed/Sydney_${date.format('YYYY-MM-DD')}.json` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.episode_title, + category: item.genres, + description: item.description, + season: parseSeason(item), + episode: parseEpisode(item), + rating: parseRating(item), + icon: parseIcon(item), + start: parseTime(item.start_time), + stop: parseTime(item.end_time) + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data.schedule)) return [] + + const channelData = data.schedule.find(i => i.channel == channel.site_id) + return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] + } catch (err) { + return [] + } +} + +function parseSeason(item) { + return item.series_num || null +} +function parseEpisode(item) { + return item.episode_num || null +} +function parseTime(time) { + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney') +} +function parseIcon(item) { + return item.image_file + ? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}` + : null +} +function parseRating(item) { + return item.rating + ? { + system: 'ACB', + value: item.rating + } + : null +} diff --git a/sites/abc.net.au/abc.net.au.test.js b/sites/abc.net.au/abc.net.au.test.js index 29e720c8..791302a7 100644 --- a/sites/abc.net.au/abc.net.au.test.js +++ b/sites/abc.net.au/abc.net.au.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=abc.net.au - -const { parser, url } = require('./abc.net.au.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ABC1', - xmltv_id: 'ABCTV.au' -} -it('can generate valid url', () => { - expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json') -}) - -it('can parse response', () => { - const content = - '{"date":"2022-12-22","region":"Sydney","schedule":[{"channel":"ABC1","listing":[{"consumer_advice":"Adult Themes, Drug Use, Violence","rating":"M","show_id":912747,"repeat":true,"description":"When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?","title":"Silent Witness","crid":"ZW2178A004S00","start_time":"2022-12-22T00:46:00","series-crid":"ZW2178A","live":false,"captioning":true,"show_type":"Episode","series_num":22,"episode_title":"Lift Up Your Hearts (part Two)","length":58,"onair_title":"Silent Witness","end_time":"2022-12-22T01:44:00","genres":["Entertainment"],"image_file":"ZW2178A004S00_460.jpg","prog_slug":"silent-witness","episode_num":4}]}]}' - - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Silent Witness', - sub_title: 'Lift Up Your Hearts (part Two)', - description: - 'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?', - category: ['Entertainment'], - rating: { - system: 'ACB', - value: 'M' - }, - season: 22, - episode: 4, - icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg', - start: '2022-12-21T13:46:00.000Z', - stop: '2022-12-21T14:44:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: - 'NoSuchKeyThe specified key does not exist.processed/Sydney_2023-01-17.json6MRHX5TJ12X39B3Y59rH6XRMrmkFywg8Kv58iqpI6O1fuOCuEbKa1HRRYa4buByXMBTvAhz8zuAK7X5D+ZN9ZuWxyGs=' - }, - channel - ) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=abc.net.au + +const { parser, url } = require('./abc.net.au.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ABC1', + xmltv_id: 'ABCTV.au' +} +it('can generate valid url', () => { + expect(url({ date })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json') +}) + +it('can parse response', () => { + const content = + '{"date":"2022-12-22","region":"Sydney","schedule":[{"channel":"ABC1","listing":[{"consumer_advice":"Adult Themes, Drug Use, Violence","rating":"M","show_id":912747,"repeat":true,"description":"When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?","title":"Silent Witness","crid":"ZW2178A004S00","start_time":"2022-12-22T00:46:00","series-crid":"ZW2178A","live":false,"captioning":true,"show_type":"Episode","series_num":22,"episode_title":"Lift Up Your Hearts (part Two)","length":58,"onair_title":"Silent Witness","end_time":"2022-12-22T01:44:00","genres":["Entertainment"],"image_file":"ZW2178A004S00_460.jpg","prog_slug":"silent-witness","episode_num":4}]}]}' + + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Silent Witness', + sub_title: 'Lift Up Your Hearts (part Two)', + description: + 'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?', + category: ['Entertainment'], + rating: { + system: 'ACB', + value: 'M' + }, + season: 22, + episode: 4, + icon: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg', + start: '2022-12-21T13:46:00.000Z', + stop: '2022-12-21T14:44:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser( + { + content: + 'NoSuchKeyThe specified key does not exist.processed/Sydney_2023-01-17.json6MRHX5TJ12X39B3Y59rH6XRMrmkFywg8Kv58iqpI6O1fuOCuEbKa1HRRYa4buByXMBTvAhz8zuAK7X5D+ZN9ZuWxyGs=' + }, + channel + ) + expect(result).toMatchObject([]) +}) diff --git a/sites/allente.se/allente.se.config.js b/sites/allente.se/allente.se.config.js index f941be0b..37d5bf36 100644 --- a/sites/allente.se/allente.se.config.js +++ b/sites/allente.se/allente.se.config.js @@ -1,63 +1,63 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'allente.se', - days: 2, - url({ date, channel }) { - const [country] = channel.site_id.split('#') - - return `https://cs-vcb.allente.${country}/epg/events?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (!item.details) return - const start = dayjs(item.time) - const stop = start.add(item.details.duration, 'm') - programs.push({ - title: item.title, - category: item.details.categories, - description: item.details.description, - icon: item.details.image, - season: parseSeason(item), - episode: parseEpisode(item), - start, - stop - }) - }) - - return programs - }, - async channels({ country, lang }) { - const data = await axios - .get(`https://cs-vcb.allente.${country}/epg/events?date=2021-11-17`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang, - site_id: `${country}#${item.id}`, - name: item.name - } - }) - } -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(i => i.id === channelId) - - return channelData && Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(item) { - return item.details.season || null -} -function parseEpisode(item) { - return item.details.episode || null -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'allente.se', + days: 2, + url({ date, channel }) { + const [country] = channel.site_id.split('#') + + return `https://cs-vcb.allente.${country}/epg/events?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (!item.details) return + const start = dayjs(item.time) + const stop = start.add(item.details.duration, 'm') + programs.push({ + title: item.title, + category: item.details.categories, + description: item.details.description, + icon: item.details.image, + season: parseSeason(item), + episode: parseEpisode(item), + start, + stop + }) + }) + + return programs + }, + async channels({ country, lang }) { + const data = await axios + .get(`https://cs-vcb.allente.${country}/epg/events?date=2021-11-17`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang, + site_id: `${country}#${item.id}`, + name: item.name + } + }) + } +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(i => i.id === channelId) + + return channelData && Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(item) { + return item.details.season || null +} +function parseEpisode(item) { + return item.details.episode || null +} diff --git a/sites/allente.se/allente.se.test.js b/sites/allente.se/allente.se.test.js index 6492b5e7..f869aff4 100644 --- a/sites/allente.se/allente.se.test.js +++ b/sites/allente.se/allente.se.test.js @@ -1,62 +1,62 @@ -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_se.channels.xml --set=country:se --set=lang:sv -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_fi.channels.xml --set=country:fi --set=lang:fi -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_no.channels.xml --set=country:no --set=lang:no -// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_dk.channels.xml --set=country:dk --set=lang:da -// npm run grab -- --site=allente.se - -const { parser, url } = require('./allente.se.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'se#0148', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17') -}) - -it('can generate valid url for different country', () => { - const dkChannel = { site_id: 'dk#0148' } - expect(url({ date, channel: dkChannel })).toBe( - 'https://cs-vcb.allente.dk/epg/events?date=2021-11-17' - ) -}) - -it('can parse response', () => { - const content = - '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-22T07:10:00.000Z', - stop: '2022-08-22T07:30:00.000Z', - title: 'Hemmagympa med Sofia', - category: ['other'], - description: - 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', - icon: 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', - season: 4, - episode: 1 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"date":"2001-11-17","categories":[],"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_se.channels.xml --set=country:se --set=lang:sv +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_fi.channels.xml --set=country:fi --set=lang:fi +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_no.channels.xml --set=country:no --set=lang:no +// npm run channels:parse -- --config=./sites/allente.se/allente.se.config.js --output=./sites/allente.se/allente.se_dk.channels.xml --set=country:dk --set=lang:da +// npm run grab -- --site=allente.se + +const { parser, url } = require('./allente.se.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'se#0148', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://cs-vcb.allente.se/epg/events?date=2021-11-17') +}) + +it('can generate valid url for different country', () => { + const dkChannel = { site_id: 'dk#0148' } + expect(url({ date, channel: dkChannel })).toBe( + 'https://cs-vcb.allente.dk/epg/events?date=2021-11-17' + ) +}) + +it('can parse response', () => { + const content = + '{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-22T07:10:00.000Z', + stop: '2022-08-22T07:30:00.000Z', + title: 'Hemmagympa med Sofia', + category: ['other'], + description: + 'Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.', + icon: 'https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440', + season: 4, + episode: 1 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"date":"2001-11-17","categories":[],"channels":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/andorradifusio.ad/andorradifusio.ad.config.js b/sites/andorradifusio.ad/andorradifusio.ad.config.js index c20b2900..cf3e5a5d 100644 --- a/sites/andorradifusio.ad/andorradifusio.ad.config.js +++ b/sites/andorradifusio.ad/andorradifusio.ad.config.js @@ -1,59 +1,59 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'andorradifusio.ad', - days: 2, - url({ channel }) { - return `https://www.andorradifusio.ad/programacio/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('MM/DD/YYYY')} ${item.time}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase() - const column = $('.programacio-dia > h3 > .dia') - .filter((i, el) => $(el).text() === day.slice(0, 6) + '.') - .first() - .parent() - .parent() - const items = [] - const titles = column.find('p').toArray() - column.find('h4').each((i, time) => { - items.push({ - time: $(time).text(), - title: $(titles[i]).text() - }) - }) - - return items -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'andorradifusio.ad', + days: 2, + url({ channel }) { + return `https://www.andorradifusio.ad/programacio/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('MM/DD/YYYY')} ${item.time}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Madrid' }).toUTC() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const day = DateTime.fromMillis(date.valueOf()).setLocale('ca').toFormat('dd LLLL').toLowerCase() + const column = $('.programacio-dia > h3 > .dia') + .filter((i, el) => $(el).text() === day.slice(0, 6) + '.') + .first() + .parent() + .parent() + const items = [] + const titles = column.find('p').toArray() + column.find('h4').each((i, time) => { + items.push({ + time: $(time).text(), + title: $(titles[i]).text() + }) + }) + + return items +} diff --git a/sites/andorradifusio.ad/andorradifusio.ad.test.js b/sites/andorradifusio.ad/andorradifusio.ad.test.js index 1e54024b..a8996138 100644 --- a/sites/andorradifusio.ad/andorradifusio.ad.test.js +++ b/sites/andorradifusio.ad/andorradifusio.ad.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=andorradifusio.ad - -const { parser, url } = require('./andorradifusio.ad.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'atv', - xmltv_id: 'AndorraTV.ad' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-07T05:00:00.000Z', - stop: '2023-06-07T06:00:00.000Z', - title: 'Club Piolet' - }) - - expect(results[20]).toMatchObject({ - start: '2023-06-07T23:00:00.000Z', - stop: '2023-06-08T00:00:00.000Z', - title: 'Àrea Andorra Difusió' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=andorradifusio.ad + +const { parser, url } = require('./andorradifusio.ad.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'atv', + xmltv_id: 'AndorraTV.ad' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.andorradifusio.ad/programacio/atv') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-07T05:00:00.000Z', + stop: '2023-06-07T06:00:00.000Z', + title: 'Club Piolet' + }) + + expect(results[20]).toMatchObject({ + start: '2023-06-07T23:00:00.000Z', + stop: '2023-06-08T00:00:00.000Z', + title: 'Àrea Andorra Difusió' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/arianaafgtv.com/arianaafgtv.com.config.js b/sites/arianaafgtv.com/arianaafgtv.com.config.js index 69a20807..17e7011c 100644 --- a/sites/arianaafgtv.com/arianaafgtv.com.config.js +++ b/sites/arianaafgtv.com/arianaafgtv.com.config.js @@ -1,82 +1,82 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'arianaafgtv.com', - days: 2, - url: 'https://www.arianaafgtv.com/index.html', - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const title = item.title - const start = parseStart(item, date) - const stop = parseStop(item, date) - programs.push({ - title, - start, - stop - }) - }) - - return programs - } -} - -function parseStop(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}` - - return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') -} - -function parseStart(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}` - - return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const dayOfWeek = date.format('dddd') - const column = $('.H4') - .filter((i, el) => { - return $(el).text() === dayOfWeek - }) - .first() - .parent() - - const rows = column - .find('.Paragraph') - .map((i, el) => { - return $(el).html() - }) - .toArray() - .map(r => (r === ' ' ? '|' : r)) - .join(' ') - .split('|') - - const items = [] - rows.forEach(row => { - row = row.trim() - if (row) { - const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi) - if (!found) return - const time = found[0] - let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1] - start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') - let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1] - end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') - const title = row.replace(time, '').replace(' ', '').trim() - items.push({ start, end, title }) - } - }) - - return items -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'arianaafgtv.com', + days: 2, + url: 'https://www.arianaafgtv.com/index.html', + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const title = item.title + const start = parseStart(item, date) + const stop = parseStop(item, date) + programs.push({ + title, + start, + stop + }) + }) + + return programs + } +} + +function parseStop(item, date) { + const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}` + + return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') +} + +function parseStart(item, date) { + const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}` + + return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul') +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const dayOfWeek = date.format('dddd') + const column = $('.H4') + .filter((i, el) => { + return $(el).text() === dayOfWeek + }) + .first() + .parent() + + const rows = column + .find('.Paragraph') + .map((i, el) => { + return $(el).html() + }) + .toArray() + .map(r => (r === ' ' ? '|' : r)) + .join(' ') + .split('|') + + const items = [] + rows.forEach(row => { + row = row.trim() + if (row) { + const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi) + if (!found) return + const time = found[0] + let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1] + start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') + let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1] + end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A') + const title = row.replace(time, '').replace(' ', '').trim() + items.push({ start, end, title }) + } + }) + + return items +} diff --git a/sites/arianatelevision.com/arianatelevision.com.config.js b/sites/arianatelevision.com/arianatelevision.com.config.js index 40bc9b79..d6284e46 100644 --- a/sites/arianatelevision.com/arianatelevision.com.config.js +++ b/sites/arianatelevision.com/arianatelevision.com.config.js @@ -1,60 +1,60 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'arianatelevision.com', - days: 2, - url: 'https://www.arianatelevision.com/program-schedule/', - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.start}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const settings = $('#jtrt_table_settings_508').text() - if (!settings) return [] - const data = JSON.parse(settings) - if (!data || !Array.isArray(data)) return [] - - let rows = data[0] - rows.shift() - const output = [] - rows.forEach(row => { - let day = date.day() + 2 - if (day > 7) day = 1 - if (!row[0] || !row[day]) return - output.push({ - start: row[0].trim(), - title: row[day].trim() - }) - }) - - return output -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'arianatelevision.com', + days: 2, + url: 'https://www.arianatelevision.com/program-schedule/', + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.start}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd H:mm', { zone: 'Asia/Kabul' }).toUTC() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const settings = $('#jtrt_table_settings_508').text() + if (!settings) return [] + const data = JSON.parse(settings) + if (!data || !Array.isArray(data)) return [] + + let rows = data[0] + rows.shift() + const output = [] + rows.forEach(row => { + let day = date.day() + 2 + if (day > 7) day = 1 + if (!row[0] || !row[day]) return + output.push({ + start: row[0].trim(), + title: row[day].trim() + }) + }) + + return output +} diff --git a/sites/arianatelevision.com/arianatelevision.com.test.js b/sites/arianatelevision.com/arianatelevision.com.test.js index d6ff3722..75f1e1cd 100644 --- a/sites/arianatelevision.com/arianatelevision.com.test.js +++ b/sites/arianatelevision.com/arianatelevision.com.test.js @@ -1,61 +1,61 @@ -// npm run grab -- --site=arianatelevision.com - -const { parser, url } = require('./arianatelevision.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'ArianaTVNational.af' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.arianatelevision.com/program-schedule/') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-27T02:30:00.000Z', - stop: '2021-11-27T03:00:00.000Z', - title: 'City Report' - }, - { - start: '2021-11-27T03:00:00.000Z', - stop: '2021-11-27T10:30:00.000Z', - title: 'ICC T20 Highlights' - }, - { - start: '2021-11-27T10:30:00.000Z', - stop: '2021-11-28T02:00:00.000Z', - title: 'ICC T20 World Cup' - }, - { - start: '2021-11-28T02:00:00.000Z', - stop: '2021-11-28T02:30:00.000Z', - title: 'Quran and Hadis' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=arianatelevision.com + +const { parser, url } = require('./arianatelevision.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'ArianaTVNational.af' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.arianatelevision.com/program-schedule/') +}) + +it('can parse response', () => { + const content = + '' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-27T02:30:00.000Z', + stop: '2021-11-27T03:00:00.000Z', + title: 'City Report' + }, + { + start: '2021-11-27T03:00:00.000Z', + stop: '2021-11-27T10:30:00.000Z', + title: 'ICC T20 Highlights' + }, + { + start: '2021-11-27T10:30:00.000Z', + stop: '2021-11-28T02:00:00.000Z', + title: 'ICC T20 World Cup' + }, + { + start: '2021-11-28T02:00:00.000Z', + stop: '2021-11-28T02:30:00.000Z', + title: 'Quran and Hadis' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/arirang.com/arirang.com.config.js b/sites/arirang.com/arirang.com.config.js index 2ee402c7..17e9b911 100644 --- a/sites/arirang.com/arirang.com.config.js +++ b/sites/arirang.com/arirang.com.config.js @@ -1,153 +1,153 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'arirang.com', - output: 'arirang.com.guide.xml', - channels: 'arirang.com.channels.xml', - lang: 'en', - days: 7, - delay: 5000, - url: 'https://www.arirang.com/v1.0/open/external/proxy', - - request: { - method: 'POST', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 }, - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - Origin: 'https://www.arirang.com', - Referer: 'https://www.arirang.com/schedule', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - }, - data: function (context) { - const { channel, date } = context - return { - address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', - method: 'POST', - headers: {}, - body: { - data: { - dmParam: { - chanId: channel.site_id, - broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'), - planNo: '1' - } - } - } - } - } - }, - - logo: function (context) { - return context.channel.logo - }, - - async parser(context) { - const programs = [] - const items = parseItems(context.content) - - for (let item of items) { - const programDetail = await parseProgramDetail(item) - - programs.push({ - title: item.displayNm, - start: parseStart(item), - stop: parseStop(item), - icon: parseIcon(programDetail), - category: parseCategory(programDetail), - description: parseDescription(programDetail) - }) - } - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek) - ? [] - : data.responseBody.dsSchWeek - } else { - return [] - } -} - -function parseStart(item) { - return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') -} - -function parseStop(item) { - return dayjs - .tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') - .add(item.broadRun, 'minute') -} - -async function parseProgramDetail(item) { - return axios - .post( - 'https://www.arirang.com/v1.0/open/program/detail', - { - bis_program_code: item.pgmCd - }, - { - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - Origin: 'https://www.arirang.com', - Referer: 'https://www.arirang.com/schedule', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - }, - timeout: 5000, - cache: { ttl: 60 * 1000 } - } - ) - .then(response => { - return response.data - }) - .catch(error => { - console.log(error) - }) -} - -function parseIcon(programDetail) { - if (programDetail && programDetail.image && programDetail.image[0].url) { - return programDetail.image[0].url - } else { - return '' - } -} - -function parseCategory(programDetail) { - if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) { - return programDetail.category_Info[0].title - } else { - return '' - } -} - -function parseDescription(programDetail) { - if ( - programDetail && - programDetail.content && - programDetail.content[0] && - programDetail.content[0].text - ) { - let description = programDetail.content[0].text - let regex = /(<([^>]+)>)/gi - return description.replace(regex, '') - } else { - return '' - } -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'arirang.com', + output: 'arirang.com.guide.xml', + channels: 'arirang.com.channels.xml', + lang: 'en', + days: 7, + delay: 5000, + url: 'https://www.arirang.com/v1.0/open/external/proxy', + + request: { + method: 'POST', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 }, + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + Origin: 'https://www.arirang.com', + Referer: 'https://www.arirang.com/schedule', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + }, + data: function (context) { + const { channel, date } = context + return { + address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', + method: 'POST', + headers: {}, + body: { + data: { + dmParam: { + chanId: channel.site_id, + broadYmd: dayjs.tz(date, 'Asia/Seoul').format('YYYYMMDD'), + planNo: '1' + } + } + } + } + } + }, + + logo: function (context) { + return context.channel.logo + }, + + async parser(context) { + const programs = [] + const items = parseItems(context.content) + + for (let item of items) { + const programDetail = await parseProgramDetail(item) + + programs.push({ + title: item.displayNm, + start: parseStart(item), + stop: parseStop(item), + icon: parseIcon(programDetail), + category: parseCategory(programDetail), + description: parseDescription(programDetail) + }) + } + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.responseBody || !Array.isArray(data.responseBody.dsSchWeek) + ? [] + : data.responseBody.dsSchWeek + } else { + return [] + } +} + +function parseStart(item) { + return dayjs.tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') +} + +function parseStop(item) { + return dayjs + .tz(item.broadYmd + ' ' + item.broadHm, 'YYYYMMDD HHmm', 'Asia/Seoul') + .add(item.broadRun, 'minute') +} + +async function parseProgramDetail(item) { + return axios + .post( + 'https://www.arirang.com/v1.0/open/program/detail', + { + bis_program_code: item.pgmCd + }, + { + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + Origin: 'https://www.arirang.com', + Referer: 'https://www.arirang.com/schedule', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' + }, + timeout: 5000, + cache: { ttl: 60 * 1000 } + } + ) + .then(response => { + return response.data + }) + .catch(error => { + console.log(error) + }) +} + +function parseIcon(programDetail) { + if (programDetail && programDetail.image && programDetail.image[0].url) { + return programDetail.image[0].url + } else { + return '' + } +} + +function parseCategory(programDetail) { + if (programDetail && programDetail.category_Info && programDetail.category_Info[0].title) { + return programDetail.category_Info[0].title + } else { + return '' + } +} + +function parseDescription(programDetail) { + if ( + programDetail && + programDetail.content && + programDetail.content[0] && + programDetail.content[0].text + ) { + let description = programDetail.content[0].text + let regex = /(<([^>]+)>)/gi + return description.replace(regex, '') + } else { + return '' + } +} diff --git a/sites/arirang.com/arirang.com.test.js b/sites/arirang.com/arirang.com.test.js index 31d1b61b..b0fd0125 100644 --- a/sites/arirang.com/arirang.com.test.js +++ b/sites/arirang.com/arirang.com.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=arirang.com -// npx jest arirang.com.test.js - -const { url, parser } = require('./arirang.com.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.tz('2023-08-25', 'Asia/Seoul').startOf('d') -const channel = { - xmltv_id: 'ArirangWorld.kr', - site_id: 'CH_W', - name: 'Arirang World', - lang: 'en', - logo: 'https://i.imgur.com/5Aoithj.png' -} -const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8') -const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8') -const context = { channel: channel, content: content, date: date } - -it('can generate valid url', () => { - expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy') -}) - -it('can handle empty guide', async () => { - const results = await parser({ channel: channel, content: '', date: date }) - expect(results).toMatchObject([]) -}) - -it('can parse response', async () => { - axios.post.mockImplementation((url, data) => { - if ( - url === 'https://www.arirang.com/v1.0/open/external/proxy' && - JSON.stringify(data) === - JSON.stringify({ - address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', - method: 'POST', - headers: {}, - body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20230825', planNo: '1' } } } - }) - ) { - return Promise.resolve({ - data: JSON.parse(content) - }) - } else if ( - url === 'https://www.arirang.com/v1.0/open/program/detail' && - JSON.stringify(data) === JSON.stringify({ bis_program_code: '2023004T' }) - ) { - return Promise.resolve({ - data: JSON.parse(programDetail) - }) - } else { - return Promise.resolve({ - data: '' - }) - } - }) - - const results = await parser(context) - - expect(results[0]).toMatchObject({ - title: 'WITHIN THE FRAME [R]', - start: dayjs.tz(date, 'Asia/Seoul'), - stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'), - icon: 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202308/2080840096998752900.png', - description: 'NEWS', - category: 'Current Affairs' - }) -}) +// npm run grab -- --site=arirang.com +// npx jest arirang.com.test.js + +const { url, parser } = require('./arirang.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.tz('2023-08-25', 'Asia/Seoul').startOf('d') +const channel = { + xmltv_id: 'ArirangWorld.kr', + site_id: 'CH_W', + name: 'Arirang World', + lang: 'en', + logo: 'https://i.imgur.com/5Aoithj.png' +} +const content = fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'), 'utf8') +const programDetail = fs.readFileSync(path.resolve(__dirname, '__data__/detail.json'), 'utf8') +const context = { channel: channel, content: content, date: date } + +it('can generate valid url', () => { + expect(url).toBe('https://www.arirang.com/v1.0/open/external/proxy') +}) + +it('can handle empty guide', async () => { + const results = await parser({ channel: channel, content: '', date: date }) + expect(results).toMatchObject([]) +}) + +it('can parse response', async () => { + axios.post.mockImplementation((url, data) => { + if ( + url === 'https://www.arirang.com/v1.0/open/external/proxy' && + JSON.stringify(data) === + JSON.stringify({ + address: 'https://script.arirang.com/api/v1/bis/listScheduleV3.do', + method: 'POST', + headers: {}, + body: { data: { dmParam: { chanId: 'CH_W', broadYmd: '20230825', planNo: '1' } } } + }) + ) { + return Promise.resolve({ + data: JSON.parse(content) + }) + } else if ( + url === 'https://www.arirang.com/v1.0/open/program/detail' && + JSON.stringify(data) === JSON.stringify({ bis_program_code: '2023004T' }) + ) { + return Promise.resolve({ + data: JSON.parse(programDetail) + }) + } else { + return Promise.resolve({ + data: '' + }) + } + }) + + const results = await parser(context) + + expect(results[0]).toMatchObject({ + title: 'WITHIN THE FRAME [R]', + start: dayjs.tz(date, 'Asia/Seoul'), + stop: dayjs.tz(date, 'Asia/Seoul').add(30, 'minute'), + icon: 'https://img.arirang.com/v1/AUTH_d52449c16d3b4bbca17d4fffd9fc44af/public/images/202308/2080840096998752900.png', + description: 'NEWS', + category: 'Current Affairs' + }) +}) diff --git a/sites/artonline.tv/artonline.tv.config.js b/sites/artonline.tv/artonline.tv.config.js index 54d5652a..f00e6fd6 100644 --- a/sites/artonline.tv/artonline.tv.config.js +++ b/sites/artonline.tv/artonline.tv.config.js @@ -1,68 +1,68 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 - -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') -const dayjs = require('dayjs') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'artonline.tv', - days: 2, - url: function ({ channel }) { - return `https://www.artonline.tv/Home/Tvlist${channel.site_id}` - }, - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data: function ({ date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - const params = new URLSearchParams() - params.append('objId', diff) - - return params - } - }, - parser: function ({ content }) { - const programs = [] - if (!content) return programs - const items = JSON.parse(content) - items.forEach(item => { - const icon = parseIcon(item) - const start = parseStart(item) - const duration = parseDuration(item) - const stop = start.add(duration, 's') - programs.push({ - title: item.title, - description: item.description, - icon, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /) - const [HH, mm] = item.start_Time.split(':') - - return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh') -} - -function parseDuration(item) { - const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/) - - return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseIcon(item) { - return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 + +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') +const dayjs = require('dayjs') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'artonline.tv', + days: 2, + url: function ({ channel }) { + return `https://www.artonline.tv/Home/Tvlist${channel.site_id}` + }, + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data: function ({ date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + const params = new URLSearchParams() + params.append('objId', diff) + + return params + } + }, + parser: function ({ content }) { + const programs = [] + if (!content) return programs + const items = JSON.parse(content) + items.forEach(item => { + const icon = parseIcon(item) + const start = parseStart(item) + const duration = parseDuration(item) + const stop = start.add(duration, 's') + programs.push({ + title: item.title, + description: item.description, + icon, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + const [, M, D, YYYY] = item.adddate.match(/(\d+)\/(\d+)\/(\d+) /) + const [HH, mm] = item.start_Time.split(':') + + return dayjs.tz(`${YYYY}-${M}-${D}T${HH}:${mm}:00`, 'YYYY-M-DTHH:mm:ss', 'Asia/Riyadh') +} + +function parseDuration(item) { + const [, HH, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)/) + + return parseInt(HH) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseIcon(item) { + return item.thumbnail ? `https://www.artonline.tv${item.thumbnail}` : null +} diff --git a/sites/artonline.tv/artonline.tv.test.js b/sites/artonline.tv/artonline.tv.test.js index 17ab430c..43e52e89 100644 --- a/sites/artonline.tv/artonline.tv.test.js +++ b/sites/artonline.tv/artonline.tv.test.js @@ -1,67 +1,67 @@ -// npm run grab -- --site=artonline.tv - -const { parser, url, request } = require('./artonline.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - site_id: 'Aflam2', - xmltv_id: 'ARTAflam2.sa' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'content-type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data for today', () => { - const date = dayjs.utc().startOf('d') - const data = request.data({ date }) - expect(data.get('objId')).toBe('0') -}) - -it('can generate valid request data for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - const data = request.data({ date }) - expect(data.get('objId')).toBe('1') -}) - -it('can parse response', () => { - const content = - '[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-03T21:30:00.000Z', - stop: '2022-03-03T23:04:00.000Z', - title: 'الراقصه و السياسي', - description: - 'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .', - icon: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=artonline.tv + +const { parser, url, request } = require('./artonline.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const channel = { + site_id: 'Aflam2', + xmltv_id: 'ARTAflam2.sa' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.artonline.tv/Home/TvlistAflam2') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data for today', () => { + const date = dayjs.utc().startOf('d') + const data = request.data({ date }) + expect(data.get('objId')).toBe('0') +}) + +it('can generate valid request data for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + const data = request.data({ date }) + expect(data.get('objId')).toBe('1') +}) + +it('can parse response', () => { + const content = + '[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-03T21:30:00.000Z', + stop: '2022-03-03T23:04:00.000Z', + title: 'الراقصه و السياسي', + description: + 'تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .', + icon: 'https://www.artonline.tv/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/astro.com.my/astro.com.my.config.js b/sites/astro.com.my/astro.com.my.config.js index 2711880a..0680838c 100644 --- a/sites/astro.com.my/astro.com.my.config.js +++ b/sites/astro.com.my/astro.com.my.config.js @@ -1,123 +1,123 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my' - -module.exports = { - site: 'astro.com.my', - days: 2, - url: function ({ channel }) { - return `${API_ENDPOINT}/channel/${channel.site_id}.json` - }, - async parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const start = dayjs.utc(item.datetimeInUtc) - const duration = parseDuration(item.duration) - const stop = start.add(duration, 's') - const details = await loadProgramDetails(item) - programs.push({ - title: details.title, - sub_title: item.subtitles, - description: details.longSynopsis || details.shortSynopsis, - actors: parseList(details.cast), - directors: parseList(details.director), - icon: details.imageUrl, - rating: parseRating(details), - categories: parseCategories(details), - episode: parseEpisode(item), - season: parseSeason(details), - start: start, - stop: stop - }) - } - - return programs - } -} - -function parseEpisode(item) { - const [, number] = item.title.match(/Ep(\d+)$/) || [null, null] - - return number ? parseInt(number) : null -} - -function parseSeason(details) { - const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null] - - return season ? parseInt(season) : null -} - -function parseList(list) { - return typeof list === 'string' ? list.split(',') : [] -} - -function parseRating(details) { - return details.certification - ? { - system: 'LPF', - value: details.certification - } - : null -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - const schedules = data.response.schedule - - return schedules[date.format('YYYY-MM-DD')] || [] - } catch (e) { - return [] - } -} - -function parseDuration(duration) { - const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/) - const hours = parseInt(match[1]) - const minutes = parseInt(match[2]) - const seconds = parseInt(match[3]) - - return hours * 3600 + minutes * 60 + seconds -} - -function parseCategories(details) { - const genres = { - 'filter/2': 'Action', - 'filter/4': 'Anime', - 'filter/12': 'Cartoons', - 'filter/16': 'Comedy', - 'filter/19': 'Crime', - 'filter/24': 'Drama', - 'filter/25': 'Educational', - 'filter/36': 'Horror', - 'filter/39': 'Live Action', - 'filter/55': 'Pre-school', - 'filter/56': 'Reality', - 'filter/60': 'Romance', - 'filter/68': 'Talk Show', - 'filter/69': 'Thriller', - 'filter/72': 'Variety', - 'filter/75': 'Series', - 'filter/100': 'Others (Children)' - } - - return Array.isArray(details.subFilter) - ? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean) - : [] -} - -async function loadProgramDetails(item) { - const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(error => console.log(error.message)) - if (!data) return {} - - return data.response || {} -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +const API_ENDPOINT = 'https://contenthub-api.eco.astro.com.my' + +module.exports = { + site: 'astro.com.my', + days: 2, + url: function ({ channel }) { + return `${API_ENDPOINT}/channel/${channel.site_id}.json` + }, + async parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const start = dayjs.utc(item.datetimeInUtc) + const duration = parseDuration(item.duration) + const stop = start.add(duration, 's') + const details = await loadProgramDetails(item) + programs.push({ + title: details.title, + sub_title: item.subtitles, + description: details.longSynopsis || details.shortSynopsis, + actors: parseList(details.cast), + directors: parseList(details.director), + icon: details.imageUrl, + rating: parseRating(details), + categories: parseCategories(details), + episode: parseEpisode(item), + season: parseSeason(details), + start: start, + stop: stop + }) + } + + return programs + } +} + +function parseEpisode(item) { + const [, number] = item.title.match(/Ep(\d+)$/) || [null, null] + + return number ? parseInt(number) : null +} + +function parseSeason(details) { + const [, season] = details.title ? details.title.match(/ S(\d+)/) || [null, null] : [null, null] + + return season ? parseInt(season) : null +} + +function parseList(list) { + return typeof list === 'string' ? list.split(',') : [] +} + +function parseRating(details) { + return details.certification + ? { + system: 'LPF', + value: details.certification + } + : null +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + const schedules = data.response.schedule + + return schedules[date.format('YYYY-MM-DD')] || [] + } catch (e) { + return [] + } +} + +function parseDuration(duration) { + const match = duration.match(/(\d{2}):(\d{2}):(\d{2})/) + const hours = parseInt(match[1]) + const minutes = parseInt(match[2]) + const seconds = parseInt(match[3]) + + return hours * 3600 + minutes * 60 + seconds +} + +function parseCategories(details) { + const genres = { + 'filter/2': 'Action', + 'filter/4': 'Anime', + 'filter/12': 'Cartoons', + 'filter/16': 'Comedy', + 'filter/19': 'Crime', + 'filter/24': 'Drama', + 'filter/25': 'Educational', + 'filter/36': 'Horror', + 'filter/39': 'Live Action', + 'filter/55': 'Pre-school', + 'filter/56': 'Reality', + 'filter/60': 'Romance', + 'filter/68': 'Talk Show', + 'filter/69': 'Thriller', + 'filter/72': 'Variety', + 'filter/75': 'Series', + 'filter/100': 'Others (Children)' + } + + return Array.isArray(details.subFilter) + ? details.subFilter.map(g => genres[g.toLowerCase()]).filter(Boolean) + : [] +} + +async function loadProgramDetails(item) { + const url = `${API_ENDPOINT}/api/v1/linear-detail?siTrafficKey=${item.siTrafficKey}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(error => console.log(error.message)) + if (!data) return {} + + return data.response || {} +} diff --git a/sites/astro.com.my/astro.com.my.test.js b/sites/astro.com.my/astro.com.my.test.js index 5c615f51..755ec3d8 100644 --- a/sites/astro.com.my/astro.com.my.test.js +++ b/sites/astro.com.my/astro.com.my.test.js @@ -1,73 +1,73 @@ -// npm run grab -- --site=astro.com.my - -const { parser, url } = require('./astro.com.my.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '425', - xmltv_id: 'TVBClassic.hk' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(31) - expect(results[0]).toMatchObject({ - start: '2022-10-30T16:10:00.000Z', - stop: '2022-10-30T17:02:00.000Z', - title: 'Triumph in the Skies S1 Ep06', - description: - 'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?', - actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'], - directors: ['Joe Ma Tak Chung'], - icon: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg', - rating: { - system: 'LPF', - value: 'U' - }, - episode: 6, - season: 1, - categories: ['Drama'] - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = await parser({ date, content }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=astro.com.my + +const { parser, url } = require('./astro.com.my.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '425', + xmltv_id: 'TVBClassic.hk' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://contenthub-api.eco.astro.com.my/channel/425.json') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://contenthub-api.eco.astro.com.my/api/v1/linear-detail?siTrafficKey=1:10000526:47979653' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(31) + expect(results[0]).toMatchObject({ + start: '2022-10-30T16:10:00.000Z', + stop: '2022-10-30T17:02:00.000Z', + title: 'Triumph in the Skies S1 Ep06', + description: + 'This classic drama depicts the many aspects of two complicated relationships set against an airline company. Will those involved ever find true love?', + actors: ['Francis Ng Chun Yu', 'Joe Ma Tak Chung', 'Flora Chan Wai San'], + directors: ['Joe Ma Tak Chung'], + icon: 'https://s3-ap-southeast-1.amazonaws.com/ams-astro/production/images/1035X328883.jpg', + rating: { + system: 'LPF', + value: 'U' + }, + episode: 6, + season: 1, + categories: ['Drama'] + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = await parser({ date, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/bein.com/bein.com.config.js b/sites/bein.com/bein.com.config.js index 8c1552ec..898ea225 100644 --- a/sites/bein.com/bein.com.config.js +++ b/sites/bein.com/bein.com.config.js @@ -1,79 +1,79 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'bein.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - const [category] = channel.site_id.split('#') - const postid = channel.lang === 'ar' ? '25344' : '25356' - - return `https://www.bein.com/${ - channel.lang - }/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format( - 'YYYY-MM-DD' - )}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 }) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - if (!title) return - const category = parseCategory($item) - const prev = programs[programs.length - 1] - let start = parseTime($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.plus({ days: 1 }) - } - prev.stop = start - } - let stop = parseTime($item, start) - if (stop < start) { - stop = stop.plus({ days: 1 }) - } - programs.push({ - title, - category, - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.title').text() -} - -function parseCategory($item) { - return $item('.format').text() -} - -function parseTime($item, date) { - let [, time] = $item('.time') - .text() - .match(/^(\d{2}:\d{2})/) || [null, null] - if (!time) return null - time = `${date.toFormat('yyyy-MM-dd')} ${time}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC() -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const $ = cheerio.load(content) - - return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'bein.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + const [category] = channel.site_id.split('#') + const postid = channel.lang === 'ar' ? '25344' : '25356' + + return `https://www.bein.com/${ + channel.lang + }/epg-ajax-template/?action=epg_fetch&category=${category}&cdate=${date.format( + 'YYYY-MM-DD' + )}&language=${channel.lang.toUpperCase()}&loadindex=0&mins=00&offset=0&postid=${postid}&serviceidentity=bein.net` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + date = DateTime.fromMillis(date.valueOf()).minus({ days: 1 }) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + if (!title) return + const category = parseCategory($item) + const prev = programs[programs.length - 1] + let start = parseTime($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.plus({ days: 1 }) + } + prev.stop = start + } + let stop = parseTime($item, start) + if (stop < start) { + stop = stop.plus({ days: 1 }) + } + programs.push({ + title, + category, + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.title').text() +} + +function parseCategory($item) { + return $item('.format').text() +} + +function parseTime($item, date) { + let [, time] = $item('.time') + .text() + .match(/^(\d{2}:\d{2})/) || [null, null] + if (!time) return null + time = `${date.toFormat('yyyy-MM-dd')} ${time}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Qatar' }).toUTC() +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const $ = cheerio.load(content) + + return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() +} diff --git a/sites/bein.com/bein.com.test.js b/sites/bein.com/bein.com.test.js index 73174712..0525afb0 100644 --- a/sites/bein.com/bein.com.test.js +++ b/sites/bein.com/bein.com.test.js @@ -1,60 +1,60 @@ -// npm run grab -- --site=bein.com - -const fs = require('fs') -const path = require('path') -const { parser, url } = require('./bein.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' } - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-18T20:15:00.000Z', - stop: '2023-01-18T22:15:00.000Z', - title: 'The Walk', - category: 'Movies' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-18T22:15:00.000Z', - stop: '2023-01-19T00:00:00.000Z', - title: 'Resident Evil: Welcome To Raccoon City', - category: 'Movies' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-19T15:30:00.000Z', - stop: '2023-01-19T18:00:00.000Z', - title: 'Spider-Man: No Way Home', - category: 'Movies' - }) -}) - -it('can handle empty guide', () => { - const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html')) - const result = parser({ - date, - channel, - content: noContent - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=bein.com + +const fs = require('fs') +const path = require('path') +const { parser, url } = require('./bein.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'entertainment#1', xmltv_id: 'beINMovies1Premiere.qa', lang: 'en' } + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.bein.com/en/epg-ajax-template/?action=epg_fetch&category=entertainment&cdate=2023-01-19&language=EN&loadindex=0&mins=00&offset=0&postid=25356&serviceidentity=bein.net' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/bein.com/__data__/content.html')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-18T20:15:00.000Z', + stop: '2023-01-18T22:15:00.000Z', + title: 'The Walk', + category: 'Movies' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-18T22:15:00.000Z', + stop: '2023-01-19T00:00:00.000Z', + title: 'Resident Evil: Welcome To Raccoon City', + category: 'Movies' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-19T15:30:00.000Z', + stop: '2023-01-19T18:00:00.000Z', + title: 'Spider-Man: No Way Home', + category: 'Movies' + }) +}) + +it('can handle empty guide', () => { + const noContent = fs.readFileSync(path.resolve('sites/bein.com/__data__/no-content.html')) + const result = parser({ + date, + channel, + content: noContent + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/beinsports.com/beinsports.com.config.js b/sites/beinsports.com/beinsports.com.config.js index 35e4783d..2b443798 100644 --- a/sites/beinsports.com/beinsports.com.config.js +++ b/sites/beinsports.com/beinsports.com.config.js @@ -1,130 +1,130 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'beinsports.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000, // 1h - interpretHeader: false - } - }, - url: function ({ date, channel }) { - let [region] = channel.site_id.split('#') - region = region ? `_${region}` : '' - - return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - let i = 0 - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - if (!title) return - const category = parseCategory($item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (i === 0 && start.hour() > 18) { - date = date.subtract(1, 'd') - start = start.subtract(1, 'd') - } - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - let stop = parseStop($item, start) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - } - - programs.push({ title, category, start, stop }) - i++ - }) - - return programs - }, - async channels({ region, lang }) { - const suffix = region ? `_${region}` : '' - const content = await axios - .get( - `https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08` - ) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(content) - const items = $('.container > div, #epg_div > div').toArray() - return items - .map(item => { - const $item = cheerio.load(item) - const id = $item('*').attr('id') - if (!/^channels_[0-9]+$/.test(id)) return null - const channelId = id.replace('channels_', '') - const imgSrc = $item('img').attr('src') - const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, ''] - - return { - lang, - site_id: `${region}#${channelId}`, - name - } - }) - .filter(i => i) - } -} - -function parseTitle($item) { - return $item('.title').text() -} - -function parseCategory($item) { - return $item('.format') - .map(function () { - return $item(this).text() - }) - .get() -} - -function parseStart($item, date) { - let time = $item('.time').text() - if (!time) return null - let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null] - if (!start) return null - start = `${date.format('YYYY-MM-DD')} ${start}${period}` - const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' - - return dayjs.tz(start, format, 'Asia/Qatar') -} - -function parseStop($item, date) { - let time = $item('.time').text() - if (!time) return null - let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null] - if (!stop) return null - stop = `${date.format('YYYY-MM-DD')} ${stop}${period}` - const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' - - return dayjs.tz(stop, format, 'Asia/Qatar') -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const $ = cheerio.load(content) - - return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'beinsports.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000, // 1h + interpretHeader: false + } + }, + url: function ({ date, channel }) { + let [region] = channel.site_id.split('#') + region = region ? `_${region}` : '' + + return `https://epg.beinsports.com/utctime${region}.php?mins=00&serviceidentity=beinsports.com&cdate=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + let i = 0 + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + if (!title) return + const category = parseCategory($item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (i === 0 && start.hour() > 18) { + date = date.subtract(1, 'd') + start = start.subtract(1, 'd') + } + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + let stop = parseStop($item, start) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + } + + programs.push({ title, category, start, stop }) + i++ + }) + + return programs + }, + async channels({ region, lang }) { + const suffix = region ? `_${region}` : '' + const content = await axios + .get( + `https://epg.beinsports.com/utctime${suffix}.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08` + ) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(content) + const items = $('.container > div, #epg_div > div').toArray() + return items + .map(item => { + const $item = cheerio.load(item) + const id = $item('*').attr('id') + if (!/^channels_[0-9]+$/.test(id)) return null + const channelId = id.replace('channels_', '') + const imgSrc = $item('img').attr('src') + const [, , name] = imgSrc.match(/(\/|)([a-z0-9-_.]+)(.png|.svg)$/i) || [null, null, ''] + + return { + lang, + site_id: `${region}#${channelId}`, + name + } + }) + .filter(i => i) + } +} + +function parseTitle($item) { + return $item('.title').text() +} + +function parseCategory($item) { + return $item('.format') + .map(function () { + return $item(this).text() + }) + .get() +} + +function parseStart($item, date) { + let time = $item('.time').text() + if (!time) return null + let [, start, period] = time.match(/^(\d{2}:\d{2})( AM| PM|)/) || [null, null, null] + if (!start) return null + start = `${date.format('YYYY-MM-DD')} ${start}${period}` + const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' + + return dayjs.tz(start, format, 'Asia/Qatar') +} + +function parseStop($item, date) { + let time = $item('.time').text() + if (!time) return null + let [, stop, period] = time.match(/(\d{2}:\d{2})( AM| PM|)$/) || [null, null, null] + if (!stop) return null + stop = `${date.format('YYYY-MM-DD')} ${stop}${period}` + const format = period ? 'YYYY-MM-DD hh:mm A' : 'YYYY-MM-DD HH:mm' + + return dayjs.tz(stop, format, 'Asia/Qatar') +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const $ = cheerio.load(content) + + return $(`#channels_${channelId} .slider > ul:first-child > li`).toArray() +} diff --git a/sites/beinsports.com/beinsports.com.test.js b/sites/beinsports.com/beinsports.com.test.js index a26de067..0289cd56 100644 --- a/sites/beinsports.com/beinsports.com.test.js +++ b/sites/beinsports.com/beinsports.com.test.js @@ -1,91 +1,91 @@ -// npm run channels:parse -- --config=./sites/beinsports.com/beinsports.com.config.js --output=./sites/beinsports.com/beinsports.com_qa-ar.channels.xml --set=lang:ar --set=region:ar -// npm run grab -- --site=beinsports.com -// npm run grab -- --site=beinsports.com - -const { parser, url } = require('./beinsports.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' } - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' - ) -}) - -it('can generate valid url for arabic guide', () => { - const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' } - const result = url({ date, channel }) - expect(result).toBe( - 'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-07T19:30:00.000Z', - stop: '2022-05-07T21:20:00.000Z', - title: 'Lorient vs Marseille', - category: ['Ligue 1 2021/22'] - }) -}) - -it('can parse response for tomorrow', () => { - const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync( - path.resolve('sites/beinsports.com/__data__/content_tomorrow.html') - ) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-08T21:20:00.000Z', - stop: '2022-05-08T23:10:00.000Z', - title: 'Celtic vs Hearts', - category: ['SPFL Premiership 2021/22'] - }) -}) - -it('can parse US response', () => { - const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html')) - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-07T20:00:00.000Z', - stop: '2022-05-07T22:00:00.000Z', - title: 'Basaksehir vs. Galatasaray', - category: ['Fútbol Turco Superliga', 'Soccer'] - }) -}) - -it('can handle empty guide', () => { - const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html')) - const result = parser({ - date, - channel, - content: noContent - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/beinsports.com/beinsports.com.config.js --output=./sites/beinsports.com/beinsports.com_qa-ar.channels.xml --set=lang:ar --set=region:ar +// npm run grab -- --site=beinsports.com +// npm run grab -- --site=beinsports.com + +const { parser, url } = require('./beinsports.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-08', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '#2', xmltv_id: 'BeINSports.qa' } + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://epg.beinsports.com/utctime.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' + ) +}) + +it('can generate valid url for arabic guide', () => { + const channel = { site_id: 'ar#1', xmltv_id: 'BeINSports.qa' } + const result = url({ date, channel }) + expect(result).toBe( + 'https://epg.beinsports.com/utctime_ar.php?mins=00&serviceidentity=beinsports.com&cdate=2022-05-08' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content.html')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-07T19:30:00.000Z', + stop: '2022-05-07T21:20:00.000Z', + title: 'Lorient vs Marseille', + category: ['Ligue 1 2021/22'] + }) +}) + +it('can parse response for tomorrow', () => { + const date = dayjs.utc('2022-05-09', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync( + path.resolve('sites/beinsports.com/__data__/content_tomorrow.html') + ) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-08T21:20:00.000Z', + stop: '2022-05-08T23:10:00.000Z', + title: 'Celtic vs Hearts', + category: ['SPFL Premiership 2021/22'] + }) +}) + +it('can parse US response', () => { + const content = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/content_us.html')) + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-07T20:00:00.000Z', + stop: '2022-05-07T22:00:00.000Z', + title: 'Basaksehir vs. Galatasaray', + category: ['Fútbol Turco Superliga', 'Soccer'] + }) +}) + +it('can handle empty guide', () => { + const noContent = fs.readFileSync(path.resolve('sites/beinsports.com/__data__/no-content.html')) + const result = parser({ + date, + channel, + content: noContent + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.config.js b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js index 773de12d..63ab575a 100644 --- a/sites/berrymedia.co.kr/berrymedia.co.kr.config.js +++ b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js @@ -1,93 +1,93 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -dayjs.Ls.en.weekStart = 1 - -module.exports = { - site: 'berrymedia.co.kr', - days: 2, - url({ channel }) { - return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest' - }, - data({ date }) { - let params = new URLSearchParams() - let startOfWeek = date.startOf('week').format('YYYY-MM-DD') - let endOfWeek = date.endOf('week').format('YYYY-MM-DD') - - params.append('week', `${startOfWeek}~${endOfWeek}`) - params.append('day', date.format('YYYY-MM-DD')) - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const time = $item('span:nth-child(1)').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseTitle($item) { - return $item('span.sdfsdf').clone().children().remove().end().text().trim() -} - -function parseCategory($item) { - return $item('span:nth-child(2) > p').text().trim() -} - -function parseRating($item) { - const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.sc_time dd').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +dayjs.Ls.en.weekStart = 1 + +module.exports = { + site: 'berrymedia.co.kr', + days: 2, + url({ channel }) { + return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + }, + data({ date }) { + let params = new URLSearchParams() + let startOfWeek = date.startOf('week').format('YYYY-MM-DD') + let endOfWeek = date.endOf('week').format('YYYY-MM-DD') + + params.append('week', `${startOfWeek}~${endOfWeek}`) + params.append('day', date.format('YYYY-MM-DD')) + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const time = $item('span:nth-child(1)').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseTitle($item) { + return $item('span.sdfsdf').clone().children().remove().end().text().trim() +} + +function parseCategory($item) { + return $item('span:nth-child(2) > p').text().trim() +} + +function parseRating($item) { + const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.sc_time dd').toArray() +} diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.test.js b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js index 88f98a75..3c20e490 100644 --- a/sites/berrymedia.co.kr/berrymedia.co.kr.test.js +++ b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=berrymedia.co.kr - -const { parser, url, request } = require('./berrymedia.co.kr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '', - xmltv_id: 'GTV.kr' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php') -}) - -it('can generate request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest' - }) -}) - -it('can generate valid request data', () => { - let params = request.data({ date }) - - expect(params.get('week')).toBe('2023-01-23~2023-01-29') - expect(params.get('day')).toBe('2023-01-26') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-25T15:00:00.000Z', - stop: '2023-01-25T16:00:00.000Z', - title: '더트롯쇼', - category: '연예/오락', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[17]).toMatchObject({ - start: '2023-01-26T13:50:00.000Z', - stop: '2023-01-26T14:20:00.000Z', - title: '나는 자연인이다', - category: '교양', - rating: { - system: 'KMRB', - value: 'ALL' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=berrymedia.co.kr + +const { parser, url, request } = require('./berrymedia.co.kr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '', + xmltv_id: 'GTV.kr' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php') +}) + +it('can generate request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest' + }) +}) + +it('can generate valid request data', () => { + let params = request.data({ date }) + + expect(params.get('week')).toBe('2023-01-23~2023-01-29') + expect(params.get('day')).toBe('2023-01-26') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-25T15:00:00.000Z', + stop: '2023-01-25T16:00:00.000Z', + title: '더트롯쇼', + category: '연예/오락', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[17]).toMatchObject({ + start: '2023-01-26T13:50:00.000Z', + stop: '2023-01-26T14:20:00.000Z', + title: '나는 자연인이다', + category: '교양', + rating: { + system: 'KMRB', + value: 'ALL' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/bt.com/bt.com.config.js b/sites/bt.com/bt.com.config.js index 297bd80c..2e7f77d1 100644 --- a/sites/bt.com/bt.com.config.js +++ b/sites/bt.com/bt.com.config.js @@ -1,54 +1,54 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'bt.com', - days: 2, - url: function ({ date, channel }) { - return `https://voila.metabroadcast.com/4/schedules/${ - channel.site_id - }.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=${date.utc().format()}&to=${date - .add(1, 'd') - .utc() - .format()}&source=api.youview.tv&annotations=content.description` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.item.title, - description: item.item.description, - icon: parseIcon(item), - season: parseSeason(item), - episode: parseEpisode(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data && data.schedule.entries ? data.schedule.entries : [] -} -function parseSeason(item) { - if (item.item.type !== 'episode') return null - return item.item.series_number || null -} -function parseEpisode(item) { - if (item.item.type !== 'episode') return null - return item.item.episode_number || null -} -function parseIcon(item) { - return item.item.image || null -} -function parseStart(item) { - return dayjs(item.broadcast.transmission_time) -} - -function parseStop(item) { - return dayjs(item.broadcast.transmission_end_time) -} +const dayjs = require('dayjs') + +module.exports = { + site: 'bt.com', + days: 2, + url: function ({ date, channel }) { + return `https://voila.metabroadcast.com/4/schedules/${ + channel.site_id + }.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=${date.utc().format()}&to=${date + .add(1, 'd') + .utc() + .format()}&source=api.youview.tv&annotations=content.description` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.item.title, + description: item.item.description, + icon: parseIcon(item), + season: parseSeason(item), + episode: parseEpisode(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data && data.schedule.entries ? data.schedule.entries : [] +} +function parseSeason(item) { + if (item.item.type !== 'episode') return null + return item.item.series_number || null +} +function parseEpisode(item) { + if (item.item.type !== 'episode') return null + return item.item.episode_number || null +} +function parseIcon(item) { + return item.item.image || null +} +function parseStart(item) { + return dayjs(item.broadcast.transmission_time) +} + +function parseStop(item) { + return dayjs(item.broadcast.transmission_end_time) +} diff --git a/sites/bt.com/bt.com.test.js b/sites/bt.com/bt.com.test.js index c7f93d65..779b722c 100644 --- a/sites/bt.com/bt.com.test.js +++ b/sites/bt.com/bt.com.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=bt.com - -const { parser, url } = require('./bt.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'hsxv', - xmltv_id: 'BBCOneHD.uk' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://voila.metabroadcast.com/4/schedules/hsxv.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=2022-03-20T00:00:00Z&to=2022-03-21T00:00:00Z&source=api.youview.tv&annotations=content.description' - ) -}) - -it('can parse response', () => { - const content = - '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[{"broadcast":{"aliases":[{"namespace":"api.youview.tv:slot","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:event-locator","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:pcrid","value":"crid://fp.bbc.co.uk/b/3Q30S2"},{"namespace":"youview:schedule_event:id","value":"79d318f3-b41a-582d-b089-7b0172538b42"}],"transmission_time":"2022-03-19T23:30:00.000Z","transmission_end_time":"2022-03-20T01:20:00.000Z","broadcast_duration":6600,"broadcast_on":"hsxv","schedule_date":null,"repeat":null,"subtitled":true,"signed":null,"audio_described":false,"high_definition":null,"widescreen":null,"surround":null,"live":null,"premiere":null,"continuation":null,"new_series":null,"new_episode":null,"new_one_off":null,"revised_repeat":null,"blackout_restriction":{"all":false}},"item":{"id":"n72nsw","type":"item","display_title":{"title":"The Finest Hours (2016)","subtitle":null},"year":null,"media_type":"video","specialization":"tv","source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"title":"The Finest Hours (2016)","description":"Drama based on a true story, recounting one of history\'s most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.","image":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D","thumbnail":null,"duration":null,"container":null}}]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' - - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'The Finest Hours (2016)', - description: - "Drama based on a true story, recounting one of history's most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.", - icon: 'https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D', - season: null, - episode: null, - start: '2022-03-19T23:30:00.000Z', - stop: '2022-03-20T01:20:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=bt.com + +const { parser, url } = require('./bt.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'hsxv', + xmltv_id: 'BBCOneHD.uk' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://voila.metabroadcast.com/4/schedules/hsxv.json?key=b4d2edb68da14dfb9e47b5465e99b1b1&from=2022-03-20T00:00:00Z&to=2022-03-21T00:00:00Z&source=api.youview.tv&annotations=content.description' + ) +}) + +it('can parse response', () => { + const content = + '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[{"broadcast":{"aliases":[{"namespace":"api.youview.tv:slot","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:event-locator","value":"dvb://233a..4484;76bc"},{"namespace":"dvb:pcrid","value":"crid://fp.bbc.co.uk/b/3Q30S2"},{"namespace":"youview:schedule_event:id","value":"79d318f3-b41a-582d-b089-7b0172538b42"}],"transmission_time":"2022-03-19T23:30:00.000Z","transmission_end_time":"2022-03-20T01:20:00.000Z","broadcast_duration":6600,"broadcast_on":"hsxv","schedule_date":null,"repeat":null,"subtitled":true,"signed":null,"audio_described":false,"high_definition":null,"widescreen":null,"surround":null,"live":null,"premiere":null,"continuation":null,"new_series":null,"new_episode":null,"new_one_off":null,"revised_repeat":null,"blackout_restriction":{"all":false}},"item":{"id":"n72nsw","type":"item","display_title":{"title":"The Finest Hours (2016)","subtitle":null},"year":null,"media_type":"video","specialization":"tv","source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"title":"The Finest Hours (2016)","description":"Drama based on a true story, recounting one of history\'s most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.","image":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D","thumbnail":null,"duration":null,"container":null}}]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' + + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'The Finest Hours (2016)', + description: + "Drama based on a true story, recounting one of history's most daring coastguard rescue attempts. Stranded on a sinking oil tanker along with 30 other sailors, engineer Ray Sybert battles to buy his crew more time as Captain Bernie Webber and three of his colleagues tackle gigantic waves and gale-force winds in their astonishing bid to save the seamen.", + icon: 'https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F52172983%2Fprimary%2F1_1024x576.jpg%3Fdefaultimg%3D0&ETag=z7ucT5kdAq7HuNQf%2FGTEJg%3D%3D', + season: null, + episode: null, + start: '2022-03-19T23:30:00.000Z', + stop: '2022-03-20T01:20:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '{"schedule":{"channel":{"title":"BBC One HD","id":"hsxv","uri":"http://api.youview.tv/channels/dvb://233a..4484","images":[{"uri":"https://images.metabroadcast.com?source=http%3A%2F%2Fimages-live.youview.tv%2Fimages%2Fentity%2F8c4c0357-d7ee-5d8a-8bc4-b177b6875128%2Fident%2F1_1024x532.png%3Fdefaultimg%3D0&ETag=r5vyecG6of%2BhCbHeEClx0Q%3D%3D","mime_type":"image/png","type":null,"color":"monochrome","theme":"light_monochrome","aspect_ratio":null,"availability_start":null,"availability_end":null,"width":1024,"height":532,"hasTitleArt":null,"source":null}],"available_from":[{"key":"api.youview.tv","name":"YouView JSON","country":"GB"}],"source":{"key":"api.youview.tv","name":"YouView JSON","country":"GB"},"same_as":[],"media_type":"video","broadcaster":null,"aliases":[{"namespace":"youview:serviceLocator","value":"dvb://233a..4484"},{"namespace":"youview:channel:id","value":"8c4c0357-d7ee-5d8a-8bc4-b177b6875128"}],"genres":[],"high_definition":true,"timeshifted":null,"regional":null,"related_links":[],"start_date":null,"advertised_from":null,"advertised_to":null,"short_description":null,"medium_description":null,"long_description":null,"region":null,"target_regions":[],"channel_type":"CHANNEL","interactive":false,"transmission_types":["DTT"],"quality":"HD","hdr":false},"source":"api.youview.tv","entries":[]},"terms_and_conditions":{"text":"Specific terms and conditions in your agreement with MetaBroadcast, and with any data provider, apply to your use of this data, and associated systems."},"results":1,"request":{"path":"/4/schedules/hsxv.json","parameters":{"annotations":"content.description","from":"2022-03-20T00:00:00Z","to":"2022-03-21T00:00:00Z","source":"api.youview.tv","key":"b4d2edb68da14dfb9e47b5465e99b1b1"}}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cablego.com.pe/cablego.com.pe.config.js b/sites/cablego.com.pe/cablego.com.pe.config.js index 1bc9eba5..5789fb37 100644 --- a/sites/cablego.com.pe/cablego.com.pe.config.js +++ b/sites/cablego.com.pe/cablego.com.pe.config.js @@ -1,109 +1,109 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'cablego.com.pe', - days: 2, - request: { - method: 'POST', - headers: { - 'x-requested-with': 'XMLHttpRequest' - }, - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel, date }) { - const [page] = channel.site_id.split('#') - - return `https://cablego.com.pe/epg/default/${date.format( - 'YYYY-MM-DD' - )}?page=${page}&do=loadPage` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const promises = [0, 1, 2, 3, 4].map(page => { - return axios.post( - `https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`, - null, - { - headers: { - 'x-requested-with': 'XMLHttpRequest' - } - } - ) - }) - - const channels = [] - await Promise.allSettled(promises).then(results => { - results.forEach((r, page) => { - if (r.status === 'fulfilled') { - const html = r.value.data.snippets['snippet--channelGrid'] - const $ = cheerio.load(html) - $('.epg-channel-strip').each((i, el) => { - const channelId = $(el).find('.epg-channel-logo').attr('id') - channels.push({ - lang: 'es', - site_id: `${page}#${channelId}`, - name: $(el).find('img').attr('alt') - }) - }) - } - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('span:nth-child(2) > a').text().trim() -} - -function parseStart($item, date) { - const time = $item('.epg-show-start').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima') -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return [] - const html = data.snippets['snippet--channelGrid'] - const $ = cheerio.load(html) - - return $(`#${channelId}`).parent().find('.epg-show').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'cablego.com.pe', + days: 2, + request: { + method: 'POST', + headers: { + 'x-requested-with': 'XMLHttpRequest' + }, + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel, date }) { + const [page] = channel.site_id.split('#') + + return `https://cablego.com.pe/epg/default/${date.format( + 'YYYY-MM-DD' + )}?page=${page}&do=loadPage` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const promises = [0, 1, 2, 3, 4].map(page => { + return axios.post( + `https://cablego.com.pe/epg/default/2022-11-28?page=${page}&do=loadPage`, + null, + { + headers: { + 'x-requested-with': 'XMLHttpRequest' + } + } + ) + }) + + const channels = [] + await Promise.allSettled(promises).then(results => { + results.forEach((r, page) => { + if (r.status === 'fulfilled') { + const html = r.value.data.snippets['snippet--channelGrid'] + const $ = cheerio.load(html) + $('.epg-channel-strip').each((i, el) => { + const channelId = $(el).find('.epg-channel-logo').attr('id') + channels.push({ + lang: 'es', + site_id: `${page}#${channelId}`, + name: $(el).find('img').attr('alt') + }) + }) + } + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('span:nth-child(2) > a').text().trim() +} + +function parseStart($item, date) { + const time = $item('.epg-show-start').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Lima') +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !data.snippets || !data.snippets['snippet--channelGrid']) return [] + const html = data.snippets['snippet--channelGrid'] + const $ = cheerio.load(html) + + return $(`#${channelId}`).parent().find('.epg-show').toArray() +} diff --git a/sites/cablego.com.pe/cablego.com.pe.test.js b/sites/cablego.com.pe/cablego.com.pe.test.js index c1da93e9..53faeaf9 100644 --- a/sites/cablego.com.pe/cablego.com.pe.test.js +++ b/sites/cablego.com.pe/cablego.com.pe.test.js @@ -1,54 +1,54 @@ -// npm run channels:parse -- --config=./sites/cablego.com.pe/cablego.com.pe.config.js --output=./sites/cablego.com.pe/cablego.com.pe.channels.xml -// npm run grab -- --site=cablego.com.pe - -const { parser, url, request } = require('./cablego.com.pe.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0#LATINA', - xmltv_id: 'Latina.pe' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-28T05:00:00.000Z', - stop: '2022-11-28T06:30:00.000Z', - title: 'Especiales Qatar' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/cablego.com.pe/cablego.com.pe.config.js --output=./sites/cablego.com.pe/cablego.com.pe.channels.xml +// npm run grab -- --site=cablego.com.pe + +const { parser, url, request } = require('./cablego.com.pe.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0#LATINA', + xmltv_id: 'Latina.pe' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://cablego.com.pe/epg/default/2022-11-28?page=0&do=loadPage' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-28T05:00:00.000Z', + stop: '2022-11-28T06:30:00.000Z', + title: 'Especiales Qatar' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cableplus.com.uy/cableplus.com.uy.config.js b/sites/cableplus.com.uy/cableplus.com.uy.config.js index a47a7bbc..017f6f45 100644 --- a/sites/cableplus.com.uy/cableplus.com.uy.config.js +++ b/sites/cableplus.com.uy/cableplus.com.uy.config.js @@ -1,133 +1,133 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.reportv.com.ar/finder' - -module.exports = { - site: 'cableplus.com.uy', - days: 2, - url: `${API_ENDPOINT}/channel`, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ date, channel }) { - const params = new URLSearchParams() - params.append('idAlineacion', '3017') - params.append('idSenial', channel.site_id) - params.append('fecha', date.format('YYYY-MM-DD')) - params.append('hora', '00:00') - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - categories: parseCategories($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const params = new URLSearchParams({ idAlineacion: '3017' }) - const data = await axios - .post(`${API_ENDPOINT}/channelGrid`, params, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } - }) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(data) - - return $('.senial') - .map(function () { - return { - lang: 'es', - site_id: $(this).attr('id'), - name: $(this).find('img').attr('alt') - } - }) - .get() - } -} - -function parseTitle($item) { - return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') - .text() - .trim() -} - -function parseIcon($item) { - return $item('img').data('src') || $item('img').attr('src') || null -} - -function parseCategories($item) { - return $item('p.evento_genero') - .map(function () { - return $item(this).text().trim() - }) - .toArray() -} - -function parseStart($item, date) { - let time = $item('.grid_fecha_hora').text().trim() - - if (time) { - return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') - } - - time = $item('.fechaHora').text().trim() - - return time - ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') - : null -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') - .filter(function () { - return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 - }) - .toArray() - let otherItems = $('#owl-pc > .item-program') - .filter(function () { - return ( - $(this) - .find('.evento_titulo > .horario > p.fechaHora') - .text() - .indexOf(date.format('DD/MM')) > -1 - ) - }) - .toArray() - - return featuredItems.concat(otherItems) -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.reportv.com.ar/finder' + +module.exports = { + site: 'cableplus.com.uy', + days: 2, + url: `${API_ENDPOINT}/channel`, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ date, channel }) { + const params = new URLSearchParams() + params.append('idAlineacion', '3017') + params.append('idSenial', channel.site_id) + params.append('fecha', date.format('YYYY-MM-DD')) + params.append('hora', '00:00') + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + categories: parseCategories($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const params = new URLSearchParams({ idAlineacion: '3017' }) + const data = await axios + .post(`${API_ENDPOINT}/channelGrid`, params, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } + }) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(data) + + return $('.senial') + .map(function () { + return { + lang: 'es', + site_id: $(this).attr('id'), + name: $(this).find('img').attr('alt') + } + }) + .get() + } +} + +function parseTitle($item) { + return $item('p.evento_titulo.texto_a_continuacion.dotdotdot,.programa-titulo > span:first-child') + .text() + .trim() +} + +function parseIcon($item) { + return $item('img').data('src') || $item('img').attr('src') || null +} + +function parseCategories($item) { + return $item('p.evento_genero') + .map(function () { + return $item(this).text().trim() + }) + .toArray() +} + +function parseStart($item, date) { + let time = $item('.grid_fecha_hora').text().trim() + + if (time) { + return dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD-MM HH:mm[hs.]', 'America/Montevideo') + } + + time = $item('.fechaHora').text().trim() + + return time + ? dayjs.tz(`${date.format('YYYY')} ${time}`, 'YYYY DD/MM HH:mm[hs.]', 'America/Montevideo') + : null +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + let featuredItems = $('.vista-pc > .programacion-fila > .channel-programa') + .filter(function () { + return $(this).find('.grid_fecha_hora').text().indexOf(date.format('DD-MM')) > -1 + }) + .toArray() + let otherItems = $('#owl-pc > .item-program') + .filter(function () { + return ( + $(this) + .find('.evento_titulo > .horario > p.fechaHora') + .text() + .indexOf(date.format('DD/MM')) > -1 + ) + }) + .toArray() + + return featuredItems.concat(otherItems) +} diff --git a/sites/cableplus.com.uy/cableplus.com.uy.test.js b/sites/cableplus.com.uy/cableplus.com.uy.test.js index 1608509d..bf00d732 100644 --- a/sites/cableplus.com.uy/cableplus.com.uy.test.js +++ b/sites/cableplus.com.uy/cableplus.com.uy.test.js @@ -1,76 +1,76 @@ -// npm run channels:parse -- --config=./sites/cableplus.com.uy/cableplus.com.uy.config.js --output=./sites/cableplus.com.uy/cableplus.com.uy.channels.xml -// npm run grab -- --site=cableplus.com.uy - -const { parser, url, request } = require('./cableplus.com.uy.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2035', - xmltv_id: 'APlusV.uy' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.reportv.com.ar/finder/channel') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const params = request.data({ date, channel }) - - expect(params.get('idAlineacion')).toBe('3017') - expect(params.get('idSenial')).toBe('2035') - expect(params.get('fecha')).toBe('2023-02-12') - expect(params.get('hora')).toBe('00:00') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(21) - - expect(results[0]).toMatchObject({ - start: '2023-02-12T09:30:00.000Z', - stop: '2023-02-12T10:30:00.000Z', - title: 'Revista agropecuaria', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', - categories: [] - }) - - expect(results[4]).toMatchObject({ - start: '2023-02-12T12:30:00.000Z', - stop: '2023-02-12T13:30:00.000Z', - title: 'De pago en pago', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', - categories: ['Cultural'] - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/cableplus.com.uy/cableplus.com.uy.config.js --output=./sites/cableplus.com.uy/cableplus.com.uy.channels.xml +// npm run grab -- --site=cableplus.com.uy + +const { parser, url, request } = require('./cableplus.com.uy.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-02-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2035', + xmltv_id: 'APlusV.uy' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.reportv.com.ar/finder/channel') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const params = request.data({ date, channel }) + + expect(params.get('idAlineacion')).toBe('3017') + expect(params.get('idSenial')).toBe('2035') + expect(params.get('fecha')).toBe('2023-02-12') + expect(params.get('hora')).toBe('00:00') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(21) + + expect(results[0]).toMatchObject({ + start: '2023-02-12T09:30:00.000Z', + stop: '2023-02-12T10:30:00.000Z', + title: 'Revista agropecuaria', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/2797844.jpg', + categories: [] + }) + + expect(results[4]).toMatchObject({ + start: '2023-02-12T12:30:00.000Z', + stop: '2023-02-12T13:30:00.000Z', + title: 'De pago en pago', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/3772835.jpg', + categories: ['Cultural'] + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js b/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js index 33595d2f..47a5d7a9 100644 --- a/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js +++ b/sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js @@ -1,93 +1,93 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'canalplus-caraibes.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://service.canal-overseas.com/ott-frontend/vector/53001/channel/${channel.site_id}/events?filter.day=${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - if (item.title === 'Fin des programmes') return - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - description: parseDescription(detail), - category: parseCategory(detail), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const html = await axios - .get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const script = $('body > script:nth-child(2)').html() - const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] - const data = JSON.parse(json) - const items = data.tvGuide.channels.byZapNumber - - return Object.values(items).map(item => { - return { - lang: 'fr', - site_id: item.epgID, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.onClick.URLPage) return {} - const url = item.onClick.URLPage - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseDescription(detail) { - return detail.detail.informations.summary || null -} - -function parseCategory(detail) { - return detail.detail.informations.subGenre || null -} -function parseIcon(item) { - return item.URLImage || item.URLImageDefault -} -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.timeSlices) return [] - const items = data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) - - return items -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'canalplus-caraibes.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://service.canal-overseas.com/ott-frontend/vector/53001/channel/${channel.site_id}/events?filter.day=${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + if (item.title === 'Fin des programmes') return + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + description: parseDescription(detail), + category: parseCategory(detail), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const html = await axios + .get('https://www.canalplus-caraibes.com/bl/guide-tv-ce-soir') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const script = $('body > script:nth-child(2)').html() + const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] + const data = JSON.parse(json) + const items = data.tvGuide.channels.byZapNumber + + return Object.values(items).map(item => { + return { + lang: 'fr', + site_id: item.epgID, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.onClick.URLPage) return {} + const url = item.onClick.URLPage + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseDescription(detail) { + return detail.detail.informations.summary || null +} + +function parseCategory(detail) { + return detail.detail.informations.subGenre || null +} +function parseIcon(item) { + return item.URLImage || item.URLImageDefault +} +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.timeSlices) return [] + const items = data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) + + return items +} diff --git a/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js b/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js index d2041367..42c1d5a7 100644 --- a/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js +++ b/sites/canalplus-caraibes.com/canalplus-caraibes.com.test.js @@ -1,137 +1,137 @@ -// [Geo-blocked] node ./scripts/channels.js --config=./sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js --output=./sites/canalplus-caraibes.com/canalplus-caraibes.com.channels.xml -// npm run grab -- --site=canalplus-caraibes.com - -const { parser, url } = require('./canalplus-caraibes.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '50115', - xmltv_id: 'beINSports1France.fr' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1' - ) -}) - -it('can parse response', done => { - const content = - '{"timeSlices":[{"contents":[{"title":"Rugby - Leinster / La Rochelle","subtitle":"Rugby","thirdTitle":"BEIN SPORTS 1 HD","startTime":1660815000,"endTime":1660816800,"onClick":{"displayTemplate":"miniDetail","displayName":"Rugby - Leinster / La Rochelle","URLPage":"https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765","URLVitrine":"https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"},"programID":224515801,"diffusionID":"140377765","URLImageDefault":"https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643","URLImage":"https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771"}],"timeSlice":"4"}]}' - axios.get.mockImplementation(url => { - if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') { - return Promise.resolve({ - data: JSON.parse(`{ - "currentPage": { - "displayName": "Rugby - Leinster / La Rochelle", - "displayTemplate": "detailPage", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" - }, - "detail": { - "informations": { - "programmeType": "EPG", - "isInOffer": false, - "isInOfferOnDevice": false, - "isInOfferForD2G": false, - "availableInVoDOnDevice": false, - "availableInVoDOnG5": false, - "availableInD2GOnDevice": false, - "availableInLiveOnDevice": false, - "rediffusions": true, - "canBeRecorded": false, - "channelName": "BEIN SPORTS 1 HD", - "startTime": 1660815000, - "endTime": 1660816800, - "title": "Rugby - Leinster / La Rochelle", - "subtitle": "Rugby", - "thirdTitle": "BEIN SPORTS 1 HD", - "genre": "Sport", - "subGenre": "Rugby", - "editorialTitle": "Sport, France, 0h30", - "audioLanguage": "VF", - "summary": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", - "summaryMedium": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", - "programID": 224515801, - "sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", - "EpgId": 50115, - "CSA": 1, - "HD": false, - "3D": false, - "diffusionID": "140377765", - "duration": "1800", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" - }, - "diffusions": [ - { - "diffusionDateUTC": 1660815000, - "sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", - "broadcastId": "140377765", - "name": "BEIN SPORTS 1 HD", - "epgID": "50115", - "ZapNumber": "191", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e" - } - ] - } - }`) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-18T09:30:00.000Z', - stop: '2022-08-18T10:00:00.000Z', - title: 'Rugby - Leinster / La Rochelle', - icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771', - category: 'Rugby', - description: - "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995." - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// [Geo-blocked] node ./scripts/channels.js --config=./sites/canalplus-caraibes.com/canalplus-caraibes.com.config.js --output=./sites/canalplus-caraibes.com/canalplus-caraibes.com.channels.xml +// npm run grab -- --site=canalplus-caraibes.com + +const { parser, url } = require('./canalplus-caraibes.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '50115', + xmltv_id: 'beINSports1France.fr' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53001/channel/50115/events?filter.day=1' + ) +}) + +it('can parse response', done => { + const content = + '{"timeSlices":[{"contents":[{"title":"Rugby - Leinster / La Rochelle","subtitle":"Rugby","thirdTitle":"BEIN SPORTS 1 HD","startTime":1660815000,"endTime":1660816800,"onClick":{"displayTemplate":"miniDetail","displayName":"Rugby - Leinster / La Rochelle","URLPage":"https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765","URLVitrine":"https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations"},"programID":224515801,"diffusionID":"140377765","URLImageDefault":"https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643","URLImage":"https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771"}],"timeSlice":"4"}]}' + axios.get.mockImplementation(url => { + if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53001/event/140377765') { + return Promise.resolve({ + data: JSON.parse(`{ + "currentPage": { + "displayName": "Rugby - Leinster / La Rochelle", + "displayTemplate": "detailPage", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" + }, + "detail": { + "informations": { + "programmeType": "EPG", + "isInOffer": false, + "isInOfferOnDevice": false, + "isInOfferForD2G": false, + "availableInVoDOnDevice": false, + "availableInVoDOnG5": false, + "availableInD2GOnDevice": false, + "availableInLiveOnDevice": false, + "rediffusions": true, + "canBeRecorded": false, + "channelName": "BEIN SPORTS 1 HD", + "startTime": 1660815000, + "endTime": 1660816800, + "title": "Rugby - Leinster / La Rochelle", + "subtitle": "Rugby", + "thirdTitle": "BEIN SPORTS 1 HD", + "genre": "Sport", + "subGenre": "Rugby", + "editorialTitle": "Sport, France, 0h30", + "audioLanguage": "VF", + "summary": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", + "summaryMedium": "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995.", + "programID": 224515801, + "sharingURL": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", + "EpgId": 50115, + "CSA": 1, + "HD": false, + "3D": false, + "diffusionID": "140377765", + "duration": "1800", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/75fca4586fdc3458930dd1ab6fc2e643", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53001/program/224515801/recommendations" + }, + "diffusions": [ + { + "diffusionDateUTC": 1660815000, + "sharingUrl": "https://www.canalplus-caraibes.com/grille-tv/event/140377765-rugby-leinster-la-rochelle.html", + "broadcastId": "140377765", + "name": "BEIN SPORTS 1 HD", + "epgID": "50115", + "ZapNumber": "191", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/4e121baf92f46b2df622c6d4f9cebf8e" + } + ] + } + }`) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-18T09:30:00.000Z', + stop: '2022-08-18T10:00:00.000Z', + title: 'Rugby - Leinster / La Rochelle', + icon: 'https://service.canal-overseas.com/image-api/v1/image/7854e20fb6efecd398598653c57cc771', + category: 'Rugby', + description: + "Retransmission d'un match de Champions Cup de rugby à XV. L'European Rugby Champions Cup est une compétition annuelle interclubs de rugby à XV disputée par les meilleures équipes en Europe. Jusqu'en 2014, cette compétition s'appelait Heineken Cup, ou H Cup, et était sous l'égide de l'ERC, et depuis cette date l'EPRC lui a succédé. La première édition s'est déroulée en 1995." + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/canalplus-haiti.com/canalplus-haiti.com.config.js b/sites/canalplus-haiti.com/canalplus-haiti.com.config.js index 86b1aff5..f9ba119f 100644 --- a/sites/canalplus-haiti.com/canalplus-haiti.com.config.js +++ b/sites/canalplus-haiti.com/canalplus-haiti.com.config.js @@ -1,94 +1,94 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'canalplus-haiti.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://service.canal-overseas.com/ott-frontend/vector/53101/channel/${channel.site_id}/events?filter.day=${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - if (item.title === 'Fin des programmes') return - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - description: parseDescription(detail), - category: parseCategory(detail), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const html = await axios - .get('https://www.canalplus-haiti.com/guide-tv-ce-soir') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const script = $('body > script:nth-child(2)').html() - const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] - const data = JSON.parse(json) - const items = data.tvGuide.channels.byZapNumber - - return Object.values(items).map(item => { - return { - lang: 'fr', - site_id: item.epgID, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.onClick.URLPage) return {} - const url = item.onClick.URLPage - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseDescription(detail) { - return detail.detail.informations.summary || null -} - -function parseCategory(detail) { - return detail.detail.informations.subGenre || null -} -function parseIcon(item) { - return item.URLImage || item.URLImageDefault -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.timeSlices) return [] - const items = data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) - - return items -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'canalplus-haiti.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://service.canal-overseas.com/ott-frontend/vector/53101/channel/${channel.site_id}/events?filter.day=${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + if (item.title === 'Fin des programmes') return + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + description: parseDescription(detail), + category: parseCategory(detail), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const html = await axios + .get('https://www.canalplus-haiti.com/guide-tv-ce-soir') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const script = $('body > script:nth-child(2)').html() + const [, json] = script.match(/window.APP_STATE=(.*);/) || [null, null] + const data = JSON.parse(json) + const items = data.tvGuide.channels.byZapNumber + + return Object.values(items).map(item => { + return { + lang: 'fr', + site_id: item.epgID, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.onClick.URLPage) return {} + const url = item.onClick.URLPage + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseDescription(detail) { + return detail.detail.informations.summary || null +} + +function parseCategory(detail) { + return detail.detail.informations.subGenre || null +} +function parseIcon(item) { + return item.URLImage || item.URLImageDefault +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.timeSlices) return [] + const items = data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) + + return items +} diff --git a/sites/canalplus-haiti.com/canalplus-haiti.com.test.js b/sites/canalplus-haiti.com/canalplus-haiti.com.test.js index 16d1d1d6..4a05e35d 100644 --- a/sites/canalplus-haiti.com/canalplus-haiti.com.test.js +++ b/sites/canalplus-haiti.com/canalplus-haiti.com.test.js @@ -1,176 +1,176 @@ -// [Geo-blocked] npm run channels:parse -- --config=./sites/canalplus-haiti.com/canalplus-haiti.com.config.js --output=./sites/canalplus-haiti.com/canalplus-haiti.com.channels.xml -// npm run grab -- --site=canalplus-haiti.com - -const { parser, url } = require('./canalplus-haiti.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '51006', - xmltv_id: 'ViaATV.mq' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1' - ) -}) - -it('can parse response', done => { - const content = `{ - "timeSlices": [ - { - "contents": [ - { - "title": "New Amsterdam - S3 - Ep7", - "subtitle": "Episode 7 - Le mur de la honte", - "thirdTitle": "viaATV", - "startTime": 1660780500, - "endTime": 1660783200, - "onClick": { - "displayTemplate": "miniDetail", - "displayName": "New Amsterdam - S3 - Ep7", - "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" - }, - "programID": 187882282, - "diffusionID": "140952809", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e" - } - ], - "timeSlice": "2" - } - ] - }` - axios.get.mockImplementation(url => { - if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') { - return Promise.resolve({ - data: JSON.parse(`{ - "currentPage": { - "displayName": "New Amsterdam - S3 - Ep7", - "displayTemplate": "detailPage", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" - }, - "detail": { - "informations": { - "programmeType": "EPG", - "isInOffer": false, - "isInOfferOnDevice": false, - "isInOfferForD2G": false, - "availableInVoDOnDevice": false, - "availableInVoDOnG5": false, - "availableInD2GOnDevice": false, - "availableInLiveOnDevice": false, - "rediffusions": true, - "canBeRecorded": false, - "channelName": "viaATV", - "startTime": 1660780500, - "endTime": 1660783200, - "title": "New Amsterdam - S3 - Ep7", - "subtitle": "Episode 7 - Le mur de la honte", - "thirdTitle": "viaATV", - "genre": "Séries", - "subGenre": "Série Hôpital", - "editorialTitle": "Séries, Etats-Unis, 2020, 0h45", - "audioLanguage": "VF", - "personnalities": [ - { - "prefix": "De :", - "content": "Darnell Martin" - }, - { - "prefix": "Avec :", - "content": "André De Shields, Anna Suzuki, Anupam Kher, Baylen Thomas, Christine Chang, Craig Wedren, Daniel Dae Kim, Dierdre Friel, Em Grosland, Emma Ramos, Freema Agyeman, Gina Gershon, Graham Norris, Jamie Ann Romero, Janet Montgomery, Jefferson Friedman, Joshua Gitta, Kerry Flanagan, Larry Bryggman, Mike Doyle, Nora Clow, Opal Clow, Ryan Eggold, Simone Policano, Stephen Spinella, Tyler Labine" - } - ], - "summary": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", - "summaryMedium": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", - "programID": 187882282, - "sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html", - "labels": { - "allocine": false, - "telerama": false, - "sensCritique": false - }, - "EpgId": 51006, - "CSA": 1, - "HD": false, - "3D": false, - "diffusionID": "140952809", - "duration": "2700", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" - }, - "diffusions": [ - { - "diffusionDateUTC": 1660780500, - "sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html", - "broadcastId": "140952809", - "name": "viaATV", - "epgID": "51006", - "ZapNumber": "28", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce" - } - ] - } - }`) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-17T23:55:00.000Z', - stop: '2022-08-18T00:40:00.000Z', - title: 'New Amsterdam - S3 - Ep7', - icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e', - category: 'Série Hôpital', - description: - "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam." - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// [Geo-blocked] npm run channels:parse -- --config=./sites/canalplus-haiti.com/canalplus-haiti.com.config.js --output=./sites/canalplus-haiti.com/canalplus-haiti.com.channels.xml +// npm run grab -- --site=canalplus-haiti.com + +const { parser, url } = require('./canalplus-haiti.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '51006', + xmltv_id: 'ViaATV.mq' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/53101/channel/51006/events?filter.day=1' + ) +}) + +it('can parse response', done => { + const content = `{ + "timeSlices": [ + { + "contents": [ + { + "title": "New Amsterdam - S3 - Ep7", + "subtitle": "Episode 7 - Le mur de la honte", + "thirdTitle": "viaATV", + "startTime": 1660780500, + "endTime": 1660783200, + "onClick": { + "displayTemplate": "miniDetail", + "displayName": "New Amsterdam - S3 - Ep7", + "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" + }, + "programID": 187882282, + "diffusionID": "140952809", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e" + } + ], + "timeSlice": "2" + } + ] + }` + axios.get.mockImplementation(url => { + if (url === 'https://service.canal-overseas.com/ott-frontend/vector/53101/event/140952809') { + return Promise.resolve({ + data: JSON.parse(`{ + "currentPage": { + "displayName": "New Amsterdam - S3 - Ep7", + "displayTemplate": "detailPage", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" + }, + "detail": { + "informations": { + "programmeType": "EPG", + "isInOffer": false, + "isInOfferOnDevice": false, + "isInOfferForD2G": false, + "availableInVoDOnDevice": false, + "availableInVoDOnG5": false, + "availableInD2GOnDevice": false, + "availableInLiveOnDevice": false, + "rediffusions": true, + "canBeRecorded": false, + "channelName": "viaATV", + "startTime": 1660780500, + "endTime": 1660783200, + "title": "New Amsterdam - S3 - Ep7", + "subtitle": "Episode 7 - Le mur de la honte", + "thirdTitle": "viaATV", + "genre": "Séries", + "subGenre": "Série Hôpital", + "editorialTitle": "Séries, Etats-Unis, 2020, 0h45", + "audioLanguage": "VF", + "personnalities": [ + { + "prefix": "De :", + "content": "Darnell Martin" + }, + { + "prefix": "Avec :", + "content": "André De Shields, Anna Suzuki, Anupam Kher, Baylen Thomas, Christine Chang, Craig Wedren, Daniel Dae Kim, Dierdre Friel, Em Grosland, Emma Ramos, Freema Agyeman, Gina Gershon, Graham Norris, Jamie Ann Romero, Janet Montgomery, Jefferson Friedman, Joshua Gitta, Kerry Flanagan, Larry Bryggman, Mike Doyle, Nora Clow, Opal Clow, Ryan Eggold, Simone Policano, Stephen Spinella, Tyler Labine" + } + ], + "summary": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", + "summaryMedium": "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam.", + "programID": 187882282, + "sharingURL": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam-s3-ep7.html", + "labels": { + "allocine": false, + "telerama": false, + "sensCritique": false + }, + "EpgId": 51006, + "CSA": 1, + "HD": false, + "3D": false, + "diffusionID": "140952809", + "duration": "2700", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/generic", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/53101/program/187882282/recommendations" + }, + "diffusions": [ + { + "diffusionDateUTC": 1660780500, + "sharingUrl": "https://www.canalplus-haiti.com/grille-tv/event/140952809-new-amsterdam.html", + "broadcastId": "140952809", + "name": "viaATV", + "epgID": "51006", + "ZapNumber": "28", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/0f67b2e85f74101c4c776cf423240fce" + } + ] + } + }`) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-17T23:55:00.000Z', + stop: '2022-08-18T00:40:00.000Z', + title: 'New Amsterdam - S3 - Ep7', + icon: 'https://service.canal-overseas.com/image-api/v1/image/52a18a209e28380b199201961c27097e', + category: 'Série Hôpital', + description: + "C'est la journée nationale de dépistage du VIH et Max offre des soins gratuits à tous les malades séropositifs qui se présentent à New Amsterdam." + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/canalplus-reunion.com/canalplus-reunion.com.config.js b/sites/canalplus-reunion.com/canalplus-reunion.com.config.js index 218e8333..54032a86 100644 --- a/sites/canalplus-reunion.com/canalplus-reunion.com.config.js +++ b/sites/canalplus-reunion.com/canalplus-reunion.com.config.js @@ -1,72 +1,72 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'canalplus-reunion.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://service.canal-overseas.com/ott-frontend/vector/63001/channel/${channel.site_id}/events?filter.day=${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - if (item.title === 'Fin des programmes') return - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - description: parseDescription(detail), - category: parseCategory(detail), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - } -} - -async function loadProgramDetails(item) { - if (!item.onClick.URLPage) return {} - const url = item.onClick.URLPage - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseDescription(detail) { - return detail.detail.informations.summary || null -} - -function parseCategory(detail) { - return detail.detail.informations.subGenre || null -} -function parseIcon(item) { - return item.URLImage || item.URLImageDefault -} -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.timeSlices) return [] - const items = data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) - - return items -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'canalplus-reunion.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://service.canal-overseas.com/ott-frontend/vector/63001/channel/${channel.site_id}/events?filter.day=${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + if (item.title === 'Fin des programmes') return + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + description: parseDescription(detail), + category: parseCategory(detail), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + } +} + +async function loadProgramDetails(item) { + if (!item.onClick.URLPage) return {} + const url = item.onClick.URLPage + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseDescription(detail) { + return detail.detail.informations.summary || null +} + +function parseCategory(detail) { + return detail.detail.informations.subGenre || null +} +function parseIcon(item) { + return item.URLImage || item.URLImageDefault +} +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.timeSlices) return [] + const items = data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) + + return items +} diff --git a/sites/canalplus-reunion.com/canalplus-reunion.com.test.js b/sites/canalplus-reunion.com/canalplus-reunion.com.test.js index c6b2cb39..cb521795 100644 --- a/sites/canalplus-reunion.com/canalplus-reunion.com.test.js +++ b/sites/canalplus-reunion.com/canalplus-reunion.com.test.js @@ -1,160 +1,160 @@ -// npm run grab -- --site=canalplus-reunion.com - -const { parser, url } = require('./canalplus-reunion.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '60243', - xmltv_id: 'beINSports2France.fr' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1' - ) -}) - -it('can parse response', done => { - const content = `{ - "timeSlices": [ - { - "contents": [ - { - "title": "Almeria / Real Madrid", - "subtitle": "Football", - "thirdTitle": "BEIN SPORTS 2 HD", - "startTime": 1660780800, - "endTime": 1660788000, - "onClick": { - "displayTemplate": "miniDetail", - "displayName": "Almeria / Real Madrid", - "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" - }, - "programID": 224523053, - "diffusionID": "140382363", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20" - } - ], - "timeSlice": "4" - } - ] - }` - axios.get.mockImplementation(url => { - if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') { - return Promise.resolve({ - data: JSON.parse(`{ - "currentPage": { - "displayName": "Almeria / Real Madrid", - "displayTemplate": "detailPage", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" - }, - "detail": { - "informations": { - "programmeType": "EPG", - "isInOffer": false, - "isInOfferOnDevice": false, - "isInOfferForD2G": false, - "availableInVoDOnDevice": false, - "availableInVoDOnG5": false, - "availableInD2GOnDevice": false, - "availableInLiveOnDevice": false, - "rediffusions": true, - "canBeRecorded": false, - "channelName": "BEIN SPORTS 2 HD", - "startTime": 1660780800, - "endTime": 1660788000, - "title": "Almeria / Real Madrid", - "subtitle": "Football", - "thirdTitle": "BEIN SPORTS 2 HD", - "genre": "Sport", - "subGenre": "Football", - "editorialTitle": "Sport, Espagne, 2h00", - "audioLanguage": "VF", - "summary": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", - "summaryMedium": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", - "programID": 224523053, - "sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", - "EpgId": 60243, - "CSA": 1, - "HD": false, - "3D": false, - "diffusionID": "140382363", - "duration": "7200", - "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", - "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", - "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" - }, - "diffusions": [ - { - "diffusionDateUTC": 1660780800, - "sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", - "broadcastId": "140382363", - "name": "BEIN SPORTS 2 HD", - "epgID": "60243", - "ZapNumber": "96", - "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", - "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9" - } - ] - } - }`) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-18T00:00:00.000Z', - stop: '2022-08-18T02:00:00.000Z', - title: 'Almeria / Real Madrid', - icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20', - category: 'Football', - description: - "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date." - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run grab -- --site=canalplus-reunion.com + +const { parser, url } = require('./canalplus-reunion.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '60243', + xmltv_id: 'beINSports2France.fr' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://service.canal-overseas.com/ott-frontend/vector/63001/channel/60243/events?filter.day=1' + ) +}) + +it('can parse response', done => { + const content = `{ + "timeSlices": [ + { + "contents": [ + { + "title": "Almeria / Real Madrid", + "subtitle": "Football", + "thirdTitle": "BEIN SPORTS 2 HD", + "startTime": 1660780800, + "endTime": 1660788000, + "onClick": { + "displayTemplate": "miniDetail", + "displayName": "Almeria / Real Madrid", + "URLPage": "https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" + }, + "programID": 224523053, + "diffusionID": "140382363", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20" + } + ], + "timeSlice": "4" + } + ] + }` + axios.get.mockImplementation(url => { + if (url === 'https://service.canal-overseas.com/ott-frontend/vector/63001/event/140382363') { + return Promise.resolve({ + data: JSON.parse(`{ + "currentPage": { + "displayName": "Almeria / Real Madrid", + "displayTemplate": "detailPage", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" + }, + "detail": { + "informations": { + "programmeType": "EPG", + "isInOffer": false, + "isInOfferOnDevice": false, + "isInOfferForD2G": false, + "availableInVoDOnDevice": false, + "availableInVoDOnG5": false, + "availableInD2GOnDevice": false, + "availableInLiveOnDevice": false, + "rediffusions": true, + "canBeRecorded": false, + "channelName": "BEIN SPORTS 2 HD", + "startTime": 1660780800, + "endTime": 1660788000, + "title": "Almeria / Real Madrid", + "subtitle": "Football", + "thirdTitle": "BEIN SPORTS 2 HD", + "genre": "Sport", + "subGenre": "Football", + "editorialTitle": "Sport, Espagne, 2h00", + "audioLanguage": "VF", + "summary": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", + "summaryMedium": "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date.", + "programID": 224523053, + "sharingURL": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", + "EpgId": 60243, + "CSA": 1, + "HD": false, + "3D": false, + "diffusionID": "140382363", + "duration": "7200", + "URLImageDefault": "https://service.canal-overseas.com/image-api/v1/image/a6b640e16608ffa3d862e2bd8a4b3e4c", + "URLImage": "https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", + "URLVitrine": "https://service.canal-overseas.com/ott-frontend/vector/63001/program/224523053/recommendations" + }, + "diffusions": [ + { + "diffusionDateUTC": 1660780800, + "sharingUrl": "https://www.canalplus-reunion.com/grille-tv/event/140382363-almeria-real-madrid.html", + "broadcastId": "140382363", + "name": "BEIN SPORTS 2 HD", + "epgID": "60243", + "ZapNumber": "96", + "URLLogo": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9", + "URLLogoBlack": "https://service.canal-overseas.com/image-api/v1/image/6e2124827406ed41236a8430352d4ed9" + } + ] + } + }`) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-18T00:00:00.000Z', + stop: '2022-08-18T02:00:00.000Z', + title: 'Almeria / Real Madrid', + icon: 'https://service.canal-overseas.com/image-api/v1/image/47000149dabce60d1769589c766aad20', + category: 'Football', + description: + "Diffusion d'un match de LaLiga Santander, championnat d'Espagne de football, la plus haute compétition de football d'Espagne. Cette compétition professionnelle, placée sous la supervision de la Fédération espagnole de football, a été fondée en 1928 et s'appelle Primera Division jusqu'en 2008. Elle se nomme ensuite Liga BBVA jusqu'en 2016 puis LaLiga Santander depuis cette date." + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + '{"currentPage":{"displayTemplate":"error","BOName":"Page introuvable"},"title":"Page introuvable","text":"La page que vous demandez est introuvable. Si le problème persiste, vous pouvez contacter l\'assistance de CANAL+/CANALSAT.","code":404}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/canalplus.com/canalplus.com.config.js b/sites/canalplus.com/canalplus.com.config.js index 1d719e26..19e42906 100644 --- a/sites/canalplus.com/canalplus.com.config.js +++ b/sites/canalplus.com/canalplus.com.config.js @@ -1,185 +1,185 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'canalplus.com', - days: 2, - url: async function ({ channel, date }) { - const [region, site_id] = channel.site_id.split('#') - const data = await axios - .get(`https://www.canalplus.com/${region}/programme-tv/`) - .then(r => r.data.toString()) - .catch(err => console.log(err)) - const token = parseToken(data) - - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - - return `https://hodor.canalplus.pro/api/v2/mycanal/channels/${token}/${site_id}/broadcasts/day/${diff}` - }, - async parser({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const details = await loadProgramDetails(item) - const info = parseInfo(details) - const start = parseStart(item) - if (prev) prev.stop = start - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - description: parseDescription(info), - icon: parseIcon(info), - actors: parseCast(info, 'Avec :'), - director: parseCast(info, 'De :'), - writer: parseCast(info, 'Scénario :'), - composer: parseCast(info, 'Musique :'), - presenter: parseCast(info, 'Présenté par :'), - date: parseDate(info), - rating: parseRating(info), - start, - stop - }) - } - - return programs - }, - async channels() { - const endpoints = { - ad: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ad/all/v2.2/globalchannels.json', - bf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bf/all/v2.2/globalchannels.json', - bi: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bi/all/v2.2/globalchannels.json', - bj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bj/all/v2.2/globalchannels.json', - bl: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/bl/all/v2.2/globalchannels.json', - cd: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cd/all/v2.2/globalchannels.json', - cf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cf/all/v2.2/globalchannels.json', - cg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cg/all/v2.2/globalchannels.json', - ch: 'https://secure-webtv-static.canal-plus.com/metadata/cpche/all/v2.2/globalchannels.json', - ci: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ci/all/v2.2/globalchannels.json', - cm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cm/all/v2.2/globalchannels.json', - cv: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cv/all/v2.2/globalchannels.json', - dj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/dj/all/v2.2/globalchannels.json', - fr: 'https://secure-webtv-static.canal-plus.com/metadata/cpfra/all/v2.2/globalchannels.json', - ga: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ga/all/v2.2/globalchannels.json', - gf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/gf/all/v2.2/globalchannels.json', - gh: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gh/all/v2.2/globalchannels.json', - gm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gm/all/v2.2/globalchannels.json', - gn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gn/all/v2.2/globalchannels.json', - gp: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gp/all/v2.2/globalchannels.json', - gw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gw/all/v2.2/globalchannels.json', - mf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mf/all/v2.2/globalchannels.json', - mg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mg/all/v2.2/globalchannels.json', - ml: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ml/all/v2.2/globalchannels.json', - mq: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mq/all/v2.2/globalchannels.json', - mr: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mr/all/v2.2/globalchannels.json', - mu: 'https://secure-webtv-static.canal-plus.com/metadata/cpmus/mu/all/v2.2/globalchannels.json', - nc: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/nc/all/v2.2/globalchannels.json', - ne: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ne/all/v2.2/globalchannels.json', - pl: 'https://secure-webtv-static.canal-plus.com/metadata/cppol/all/v2.2/globalchannels.json', - re: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/re/all/v2.2/globalchannels.json', - rw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/rw/all/v2.2/globalchannels.json', - sl: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sl/all/v2.2/globalchannels.json', - sn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sn/all/v2.2/globalchannels.json', - td: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/td/all/v2.2/globalchannels.json', - tg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/tg/all/v2.2/globalchannels.json', - wf: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/wf/all/v2.2/globalchannels.json', - yt: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/yt/all/v2.2/globalchannels.json' - } - - let channels = [] - for (let [region, url] of Object.entries(endpoints)) { - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - data.channels.forEach(channel => { - const site_id = region === 'fr' ? `#${channel.id}` : `${region}#${channel.id}` - - if (channel.name === '.') return - - channels.push({ - lang: 'fr', - site_id, - name: channel.name - }) - }) - } - - return channels - } -} - -function parseToken(data) { - const [, token] = data.match(/"token":"([^"]+)/) || [null, null] - - return token -} - -function parseStart(item) { - return item && item.startTime ? dayjs(item.startTime) : null -} - -function parseIcon(info) { - return info ? info.URLImage : null -} - -function parseDescription(info) { - return info ? info.summary : null -} - -function parseInfo(data) { - if (!data || !data.detail || !data.detail.informations) return null - - return data.detail.informations -} - -async function loadProgramDetails(item) { - if (!item.onClick || !item.onClick.URLPage) return {} - - return await axios - .get(item.onClick.URLPage) - .then(r => r.data) - .catch(console.error) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.timeSlices)) return [] - - return data.timeSlices.reduce((acc, curr) => { - acc = acc.concat(curr.contents) - return acc - }, []) -} - -function parseCast(info, type) { - let people = [] - if (info && info.personnalities) { - const personnalities = info.personnalities.find(i => i.prefix == type) - if (!personnalities) return people - for (let person of personnalities.personnalitiesList) { - people.push(person.title) - } - } - return people -} - -function parseDate(info) { - return info && info.productionYear ? info.productionYear : null -} - -function parseRating(info) { - if (!info || !info.parentalRatings) return null - let rating = info.parentalRatings.find(i => i.authority === 'CSA') - if (!rating || Array.isArray(rating)) return null - if (rating.value === '1') return null - if (rating.value === '2') rating.value = '-10' - if (rating.value === '3') rating.value = '-12' - if (rating.value === '4') rating.value = '-16' - if (rating.value === '5') rating.value = '-18' - return { - system: rating.authority, - value: rating.value - } -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'canalplus.com', + days: 2, + url: async function ({ channel, date }) { + const [region, site_id] = channel.site_id.split('#') + const data = await axios + .get(`https://www.canalplus.com/${region}/programme-tv/`) + .then(r => r.data.toString()) + .catch(err => console.log(err)) + const token = parseToken(data) + + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + + return `https://hodor.canalplus.pro/api/v2/mycanal/channels/${token}/${site_id}/broadcasts/day/${diff}` + }, + async parser({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const details = await loadProgramDetails(item) + const info = parseInfo(details) + const start = parseStart(item) + if (prev) prev.stop = start + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + description: parseDescription(info), + icon: parseIcon(info), + actors: parseCast(info, 'Avec :'), + director: parseCast(info, 'De :'), + writer: parseCast(info, 'Scénario :'), + composer: parseCast(info, 'Musique :'), + presenter: parseCast(info, 'Présenté par :'), + date: parseDate(info), + rating: parseRating(info), + start, + stop + }) + } + + return programs + }, + async channels() { + const endpoints = { + ad: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ad/all/v2.2/globalchannels.json', + bf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bf/all/v2.2/globalchannels.json', + bi: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bi/all/v2.2/globalchannels.json', + bj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/bj/all/v2.2/globalchannels.json', + bl: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/bl/all/v2.2/globalchannels.json', + cd: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cd/all/v2.2/globalchannels.json', + cf: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cf/all/v2.2/globalchannels.json', + cg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cg/all/v2.2/globalchannels.json', + ch: 'https://secure-webtv-static.canal-plus.com/metadata/cpche/all/v2.2/globalchannels.json', + ci: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ci/all/v2.2/globalchannels.json', + cm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cm/all/v2.2/globalchannels.json', + cv: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/cv/all/v2.2/globalchannels.json', + dj: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/dj/all/v2.2/globalchannels.json', + fr: 'https://secure-webtv-static.canal-plus.com/metadata/cpfra/all/v2.2/globalchannels.json', + ga: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ga/all/v2.2/globalchannels.json', + gf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/gf/all/v2.2/globalchannels.json', + gh: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gh/all/v2.2/globalchannels.json', + gm: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gm/all/v2.2/globalchannels.json', + gn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gn/all/v2.2/globalchannels.json', + gp: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gp/all/v2.2/globalchannels.json', + gw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/gw/all/v2.2/globalchannels.json', + mf: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mf/all/v2.2/globalchannels.json', + mg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mg/all/v2.2/globalchannels.json', + ml: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ml/all/v2.2/globalchannels.json', + mq: 'https://secure-webtv-static.canal-plus.com/metadata/cpant/mq/all/v2.2/globalchannels.json', + mr: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/mr/all/v2.2/globalchannels.json', + mu: 'https://secure-webtv-static.canal-plus.com/metadata/cpmus/mu/all/v2.2/globalchannels.json', + nc: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/nc/all/v2.2/globalchannels.json', + ne: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/ne/all/v2.2/globalchannels.json', + pl: 'https://secure-webtv-static.canal-plus.com/metadata/cppol/all/v2.2/globalchannels.json', + re: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/re/all/v2.2/globalchannels.json', + rw: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/rw/all/v2.2/globalchannels.json', + sl: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sl/all/v2.2/globalchannels.json', + sn: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/sn/all/v2.2/globalchannels.json', + td: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/td/all/v2.2/globalchannels.json', + tg: 'https://secure-webtv-static.canal-plus.com/metadata/cpafr/tg/all/v2.2/globalchannels.json', + wf: 'https://secure-webtv-static.canal-plus.com/metadata/cpncl/wf/all/v2.2/globalchannels.json', + yt: 'https://secure-webtv-static.canal-plus.com/metadata/cpreu/yt/all/v2.2/globalchannels.json' + } + + let channels = [] + for (let [region, url] of Object.entries(endpoints)) { + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + data.channels.forEach(channel => { + const site_id = region === 'fr' ? `#${channel.id}` : `${region}#${channel.id}` + + if (channel.name === '.') return + + channels.push({ + lang: 'fr', + site_id, + name: channel.name + }) + }) + } + + return channels + } +} + +function parseToken(data) { + const [, token] = data.match(/"token":"([^"]+)/) || [null, null] + + return token +} + +function parseStart(item) { + return item && item.startTime ? dayjs(item.startTime) : null +} + +function parseIcon(info) { + return info ? info.URLImage : null +} + +function parseDescription(info) { + return info ? info.summary : null +} + +function parseInfo(data) { + if (!data || !data.detail || !data.detail.informations) return null + + return data.detail.informations +} + +async function loadProgramDetails(item) { + if (!item.onClick || !item.onClick.URLPage) return {} + + return await axios + .get(item.onClick.URLPage) + .then(r => r.data) + .catch(console.error) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.timeSlices)) return [] + + return data.timeSlices.reduce((acc, curr) => { + acc = acc.concat(curr.contents) + return acc + }, []) +} + +function parseCast(info, type) { + let people = [] + if (info && info.personnalities) { + const personnalities = info.personnalities.find(i => i.prefix == type) + if (!personnalities) return people + for (let person of personnalities.personnalitiesList) { + people.push(person.title) + } + } + return people +} + +function parseDate(info) { + return info && info.productionYear ? info.productionYear : null +} + +function parseRating(info) { + if (!info || !info.parentalRatings) return null + let rating = info.parentalRatings.find(i => i.authority === 'CSA') + if (!rating || Array.isArray(rating)) return null + if (rating.value === '1') return null + if (rating.value === '2') rating.value = '-10' + if (rating.value === '3') rating.value = '-12' + if (rating.value === '4') rating.value = '-16' + if (rating.value === '5') rating.value = '-18' + return { + system: rating.authority, + value: rating.value + } +} diff --git a/sites/canalplus.com/canalplus.com.test.js b/sites/canalplus.com/canalplus.com.test.js index 7b905e5b..c3437908 100644 --- a/sites/canalplus.com/canalplus.com.test.js +++ b/sites/canalplus.com/canalplus.com.test.js @@ -1,147 +1,147 @@ -// npm run channels:parse -- --config=./sites/canalplus.com/canalplus.com.config.js --output=./sites/canalplus.com/canalplus.com.channels.xml -// npm run grab -- --site=canalplus.com - -const { parser, url } = require('./canalplus.com.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) -jest.mock('axios') - -const channel = { - site_id: 'bi#198', - xmltv_id: 'CanalPlusCinemaFrance.fr' -} - -it('can generate valid url for today', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.canalplus.com/bi/programme-tv/') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const today = dayjs.utc().startOf('d') - url({ channel, date: today }) - .then(result => { - expect(result).toBe( - 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' - ) - done() - }) - .catch(done) -}) - -it('can generate valid url for tomorrow', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.canalplus.com/bi/programme-tv/') { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const tomorrow = dayjs.utc().startOf('d').add(1, 'd') - url({ channel, date: tomorrow }) - .then(result => { - expect(result).toBe( - 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' - ) - done() - }) - .catch(done) -}) - -it('can parse response', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if ( - url === - 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content }) - .then(result => { - result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-01-12T06:28:00.000Z', - stop: '2023-01-12T12:06:00.000Z', - title: 'Le cercle', - description: - "Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.", - icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573', - presenter: ['Lily Bloom'], - rating: { - system: 'CSA', - value: '-10' - } - }, - { - start: '2023-01-12T12:06:00.000Z', - stop: '2023-01-12T13:06:00.000Z', - title: 'Illusions perdues', - description: - "Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...", - icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485', - director: ['Xavier Giannoli'], - actors: [ - 'Benjamin Voisin', - 'Cécile de France', - 'Vincent Lacoste', - 'Xavier Dolan', - 'Gérard Depardieu', - 'Salomé Dewaels', - 'Jeanne Balibar', - 'Louis-Do de Lencquesaing', - 'Alexis Barbosa', - 'Jean-François Stévenin', - 'André Marcon', - 'Marie Cornillon' - ], - writer: ['Xavier Giannoli'], - rating: { - system: 'CSA', - value: '-10' - } - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = await parser({ content }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/canalplus.com/canalplus.com.config.js --output=./sites/canalplus.com/canalplus.com.channels.xml +// npm run grab -- --site=canalplus.com + +const { parser, url } = require('./canalplus.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) +jest.mock('axios') + +const channel = { + site_id: 'bi#198', + xmltv_id: 'CanalPlusCinemaFrance.fr' +} + +it('can generate valid url for today', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.canalplus.com/bi/programme-tv/') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const today = dayjs.utc().startOf('d') + url({ channel, date: today }) + .then(result => { + expect(result).toBe( + 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/0' + ) + done() + }) + .catch(done) +}) + +it('can generate valid url for tomorrow', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.canalplus.com/bi/programme-tv/') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/programme-tv.html')) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const tomorrow = dayjs.utc().startOf('d').add(1, 'd') + url({ channel, date: tomorrow }) + .then(result => { + expect(result).toBe( + 'https://hodor.canalplus.pro/api/v2/mycanal/channels/f000c6f4ebf44647682b3a0fa66d7d99/198/broadcasts/day/1' + ) + done() + }) + .catch(done) +}) + +it('can parse response', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/6564630_50001.json?detailType=detailSeason&objectType=season&broadcastID=PLM_1196447642&episodeId=20482220_50001&brandID=4501558_50001&fromDiff=true' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if ( + url === + 'https://hodor.canalplus.pro/api/v2/mycanal/detail/f000c6f4ebf44647682b3a0fa66d7d99/okapi/17230453_50001.json?detailType=detailPage&objectType=unit&broadcastID=PLM_1196447637&fromDiff=true' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content }) + .then(result => { + result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-01-12T06:28:00.000Z', + stop: '2023-01-12T12:06:00.000Z', + title: 'Le cercle', + description: + "Tant qu'il y aura du cinéma, LE CERCLE sera là. C'est la seule émission télévisée de débats critiques 100% consacrée au cinéma et elle rentre dans sa 18e saison. Chaque semaine, elle offre des joutes enflammées, joyeuses et sans condescendance, sur les films à l'affiche ; et invite avec \"Le questionnaire du CERCLE\" les réalisatrices et réalisateurs à venir partager leur passion cinéphile.", + icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107297573', + presenter: ['Lily Bloom'], + rating: { + system: 'CSA', + value: '-10' + } + }, + { + start: '2023-01-12T12:06:00.000Z', + stop: '2023-01-12T13:06:00.000Z', + title: 'Illusions perdues', + description: + "Pendant la Restauration, Lucien de Rubempré, jeune provincial d'Angoulême, se rêve poète. Il débarque à Paris en quête de gloire. Il a le soutien de Louise de Bargeton, une aristocrate qui croit en son talent. Pour gagner sa vie, Lucien trouve un emploi dans le journal dirigé par le peu scrupuleux Etienne Lousteau...", + icon: 'https://thumb.canalplus.pro/http/unsafe/{resolutionXY}/filters:quality({imageQualityPercentage})/img-hapi.canalplus.pro:80/ServiceImage/ImageID/107356485', + director: ['Xavier Giannoli'], + actors: [ + 'Benjamin Voisin', + 'Cécile de France', + 'Vincent Lacoste', + 'Xavier Dolan', + 'Gérard Depardieu', + 'Salomé Dewaels', + 'Jeanne Balibar', + 'Louis-Do de Lencquesaing', + 'Alexis Barbosa', + 'Jean-François Stévenin', + 'André Marcon', + 'Marie Cornillon' + ], + writer: ['Xavier Giannoli'], + rating: { + system: 'CSA', + value: '-10' + } + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = await parser({ content }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cgates.lt/cgates.lt.config.js b/sites/cgates.lt/cgates.lt.config.js index efdc3ca2..0be507df 100644 --- a/sites/cgates.lt/cgates.lt.config.js +++ b/sites/cgates.lt/cgates.lt.config.js @@ -1,92 +1,92 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'cgates.lt', - days: 2, - url: function ({ channel }) { - return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let html = await axios - .get('https://www.cgates.lt/televizija/tv-programa-savaitei/') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const items = $('.kanalas_wrap').toArray() - - return items.map(item => { - const name = $(item).find('h6').text().trim() - const link = $(item).find('a').attr('href') - const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] - - return { - lang: 'lt', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() - - return title || $item('td:nth-child(2)').text().trim() -} - -function parseDescription($item) { - return $item('.vc_toggle_content > p').text().trim() -} - -function parseStart($item, date) { - const time = $item('.laikas') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - const section = $( - 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' - ) - .filter(function () { - return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 - }) - .first() - - return $('.tv_programa tr', section).toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'cgates.lt', + days: 2, + url: function ({ channel }) { + return `https://www.cgates.lt/tv-kanalai/${channel.site_id}/` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let html = await axios + .get('https://www.cgates.lt/televizija/tv-programa-savaitei/') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const items = $('.kanalas_wrap').toArray() + + return items.map(item => { + const name = $(item).find('h6').text().trim() + const link = $(item).find('a').attr('href') + const [, site_id] = link.match(/\/tv-kanalai\/(.*)\//) || [null, null] + + return { + lang: 'lt', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + const title = $item('td:nth-child(2) > .vc_toggle > .vc_toggle_title').text().trim() + + return title || $item('td:nth-child(2)').text().trim() +} + +function parseDescription($item) { + return $item('.vc_toggle_content > p').text().trim() +} + +function parseStart($item, date) { + const time = $item('.laikas') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Vilnius') +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + const section = $( + 'article > div:nth-child(2) > div.vc_row.wpb_row.vc_row-fluid > div > div > div > div > div' + ) + .filter(function () { + return $(`.dt-fancy-title:contains("${date.format('YYYY-MM-DD')}")`, this).length === 1 + }) + .first() + + return $('.tv_programa tr', section).toArray() +} diff --git a/sites/cgates.lt/cgates.lt.test.js b/sites/cgates.lt/cgates.lt.test.js index ac1abbec..f202b304 100644 --- a/sites/cgates.lt/cgates.lt.test.js +++ b/sites/cgates.lt/cgates.lt.test.js @@ -1,52 +1,52 @@ -// npm run channels:parse -- --config=./sites/cgates.lt/cgates.lt.config.js --output=./sites/cgates.lt/cgates.lt.channels.xml -// npm run grab -- --site=cgates.lt - -const { parser, url } = require('./cgates.lt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lrt-televizija-hd', - xmltv_id: 'LRTTV.lt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - start: '2022-08-29T21:05:00.000Z', - stop: '2022-08-29T21:30:00.000Z', - title: '31-oji nuovada (District 31), Drama, 2016', - description: - 'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas – intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.' - }) - - expect(results[34]).toMatchObject({ - start: '2022-08-30T20:45:00.000Z', - stop: '2022-08-30T21:15:00.000Z', - title: '31-oji nuovada (District 31), Drama, 2016!' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/cgates.lt/cgates.lt.config.js --output=./sites/cgates.lt/cgates.lt.channels.xml +// npm run grab -- --site=cgates.lt + +const { parser, url } = require('./cgates.lt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lrt-televizija-hd', + xmltv_id: 'LRTTV.lt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.cgates.lt/tv-kanalai/lrt-televizija-hd/') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + start: '2022-08-29T21:05:00.000Z', + stop: '2022-08-29T21:30:00.000Z', + title: '31-oji nuovada (District 31), Drama, 2016', + description: + 'Seriale pasakojama apie kasdienius policijos išbandymus ir sunkumus. Vadovybė pertvarko Monrealio miesto policijos struktūrą: išskirsto į 36 policijos nuovadas, kad šios būtų arčiau gyventojų. 31-osios nuovados darbuotojams tenka kone sunkiausias darbas: šiame miesto rajone gyvena socialiai remtinos šeimos, nuolat kovojančios su turtingųjų klase, įsipliekia ir rasinių konfliktų. Be to, čia akivaizdus kartų atotrūkis, o tapti nusikalstamo pasaulio dalimi labai lengva. Serialo siužetas – intensyvus, nauji nusikaltimai tiriami kiekvieną savaitę. Čia vaizduojamas nepagražintas nusikalstamas pasaulis, jo poveikis rajono gyventojams. Policijos nuovados darbuotojai narplios įvairiausių nusikaltimų schemas. Tai ir pagrobimai, įsilaužimai, žmogžudystės, smurtas artimoje aplinkoje, lytiniai nusikaltimai, prekyba narkotikais, teroristinių išpuolių grėsmė ir pan. Šis serialas leis žiūrovui įsigilinti į policijos pareigūnų realybę, pateiks skirtingą požiūrį į kiekvieną nusikaltimą.' + }) + + expect(results[34]).toMatchObject({ + start: '2022-08-30T20:45:00.000Z', + stop: '2022-08-30T21:15:00.000Z', + title: '31-oji nuovada (District 31), Drama, 2016!' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js index 7f8c46a2..66ae219c 100644 --- a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js +++ b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.config.js @@ -1,47 +1,47 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'chaines-tv.orange.fr', - days: 2, - url({ channel, date }) { - return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date - .add(1, 'd') - .valueOf()}&after=${channel.site_id}&limit=1` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item, start) - programs.push({ - title: item.title, - category: item.genreDetailed, - description: item.synopsis, - icon: parseIcon(item), - start: start.toJSON(), - stop: stop.toJSON() - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.covers && item.covers.length ? item.covers[0].url : null -} - -function parseStart(item) { - return dayjs.unix(item.diffusionDate) -} - -function parseStop(item, start) { - return start.add(item.duration, 's') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - - return data && data[channel.site_id] ? data[channel.site_id] : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'chaines-tv.orange.fr', + days: 2, + url({ channel, date }) { + return `https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=${date.valueOf()},${date + .add(1, 'd') + .valueOf()}&after=${channel.site_id}&limit=1` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item, start) + programs.push({ + title: item.title, + category: item.genreDetailed, + description: item.synopsis, + icon: parseIcon(item), + start: start.toJSON(), + stop: stop.toJSON() + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.covers && item.covers.length ? item.covers[0].url : null +} + +function parseStart(item) { + return dayjs.unix(item.diffusionDate) +} + +function parseStop(item, start) { + return start.add(item.duration, 's') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + + return data && data[channel.site_id] ? data[channel.site_id] : [] +} diff --git a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js index ff1ec431..9e2fc529 100644 --- a/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js +++ b/sites/chaines-tv.orange.fr/chaines-tv.orange.fr.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=chaines-tv.orange.fr - -const { parser, url } = require('./chaines-tv.orange.fr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '192', - xmltv_id: 'TF1.fr' -} -const content = - '{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d\'un de ses vieux amis.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}' - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe( - 'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T23:35:00.000Z', - stop: '2021-11-08T00:20:00.000Z', - title: 'Tête de liste', - description: - "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", - category: 'Série Suspense', - icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=chaines-tv.orange.fr + +const { parser, url } = require('./chaines-tv.orange.fr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '192', + xmltv_id: 'TF1.fr' +} +const content = + '{"192":[{"id":1635062528017,"programType":"EPISODE","title":"Tête de liste","channelId":"192","channelZappingNumber":11,"covers":[{"format":"RATIO_16_9","url":"https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg"},{"format":"RATIO_4_3","url":"https://proxymedia.woopic.com/340/p/43_EMI_9697669.jpg"}],"diffusionDate":1636328100,"duration":2700,"csa":2,"synopsis":"Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d\'un de ses vieux amis.","languageVersion":"VM","hearingImpaired":true,"audioDescription":false,"season":{"number":10,"episodesCount":23,"serie":{"title":"Esprits criminels"}},"episodeNumber":12,"definition":"SD","links":[{"rel":"SELF","href":"https://rp-live.orange.fr/live-webapp/v3/applications/STB4PC/programs/1635062528017"}],"dayPart":"OTHER","catchupId":null,"genre":"Série","genreDetailed":"Série Suspense"}]}' + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe( + 'https://rp-ott-mediation-tv.woopic.com/api-gw/live/v3/applications/STB4PC/programs?groupBy=channel&includeEmptyChannels=false&period=1636329600000,1636416000000&after=192&limit=1' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T23:35:00.000Z', + stop: '2021-11-08T00:20:00.000Z', + title: 'Tête de liste', + description: + "Un tueur en série prend un plaisir pervers à prévenir les autorités de Tallahassee avant chaque nouveau meurtre. Rossi apprend le décès d'un de ses vieux amis.", + category: 'Série Suspense', + icon: 'https://proxymedia.woopic.com/340/p/169_EMI_9697669.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/clickthecity.com/clickthecity.com.config.js b/sites/clickthecity.com/clickthecity.com.config.js index 917495ea..7855a546 100644 --- a/sites/clickthecity.com/clickthecity.com.config.js +++ b/sites/clickthecity.com/clickthecity.com.config.js @@ -1,99 +1,99 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'clickthecity.com', - days: 2, - url({ channel }) { - return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` - }, - request: { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - data({ date }) { - const params = new URLSearchParams() - params.append( - 'optDate', - DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') - ) - params.append('optTime', '00:00:00') - - return params - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - let stop = parseStop($item, date) - if (!start || !stop) return - if (start > stop) { - stop = stop.plus({ days: 1 }) - } - - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.clickthecity.com/tv/channels/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('#channels .col').toArray() - - return items.map(item => { - const name = $(item).find('.card-body').text().trim() - const url = $(item).find('a').attr('href') - const [, site_id] = url.match(/netid=(\d+)/) || [null, null] - - return { - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('td > a').text().trim() -} - -function parseStart($item, date) { - const url = $item('td.cPrg > a').attr('href') || '' - let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() -} - -function parseStop($item, date) { - const url = $item('td.cPrg > a').attr('href') || '' - let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#tvlistings > tbody > tr') - .filter(function () { - return $(this).find('td.cPrg').length - }) - .toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'clickthecity.com', + days: 2, + url({ channel }) { + return `https://www.clickthecity.com/tv/channels/?netid=${channel.site_id}` + }, + request: { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + data({ date }) { + const params = new URLSearchParams() + params.append( + 'optDate', + DateTime.fromMillis(date.valueOf()).setZone('Asia/Manila').toFormat('yyyy-MM-dd') + ) + params.append('optTime', '00:00:00') + + return params + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + let stop = parseStop($item, date) + if (!start || !stop) return + if (start > stop) { + stop = stop.plus({ days: 1 }) + } + + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.clickthecity.com/tv/channels/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('#channels .col').toArray() + + return items.map(item => { + const name = $(item).find('.card-body').text().trim() + const url = $(item).find('a').attr('href') + const [, site_id] = url.match(/netid=(\d+)/) || [null, null] + + return { + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('td > a').text().trim() +} + +function parseStart($item, date) { + const url = $item('td.cPrg > a').attr('href') || '' + let [, time] = url.match(/starttime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() +} + +function parseStop($item, date) { + const url = $item('td.cPrg > a').attr('href') || '' + let [, time] = url.match(/endtime=(\d{1,2}%3A\d{2}\+(AM|PM))/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time.replace('%3A', ':').replace('+', ' ')}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd h:mm a', { zone: 'Asia/Manila' }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#tvlistings > tbody > tr') + .filter(function () { + return $(this).find('td.cPrg').length + }) + .toArray() +} diff --git a/sites/clickthecity.com/clickthecity.com.test.js b/sites/clickthecity.com/clickthecity.com.test.js index 33ae1deb..46ae823b 100644 --- a/sites/clickthecity.com/clickthecity.com.test.js +++ b/sites/clickthecity.com/clickthecity.com.test.js @@ -1,70 +1,70 @@ -// npm run channels:parse -- --config=./sites/clickthecity.com/clickthecity.com.config.js --output=./sites/clickthecity.com/clickthecity.com.channels.xml -// npm run grab -- --site=clickthecity.com - -const { parser, url, request } = require('./clickthecity.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5', - xmltv_id: 'TV5.ph' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'content-type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date }) - expect(result.get('optDate')).toBe('2023-06-12') - expect(result.get('optTime')).toBe('00:00:00') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(20) - - expect(results[0]).toMatchObject({ - start: '2023-06-11T21:00:00.000Z', - stop: '2023-06-11T22:00:00.000Z', - title: 'Word Of God' - }) - - expect(results[19]).toMatchObject({ - start: '2023-06-12T15:30:00.000Z', - stop: '2023-06-12T16:00:00.000Z', - title: 'La Suerte De Loli' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/clickthecity.com/clickthecity.com.config.js --output=./sites/clickthecity.com/clickthecity.com.channels.xml +// npm run grab -- --site=clickthecity.com + +const { parser, url, request } = require('./clickthecity.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5', + xmltv_id: 'TV5.ph' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.clickthecity.com/tv/channels/?netid=5') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'content-type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date }) + expect(result.get('optDate')).toBe('2023-06-12') + expect(result.get('optTime')).toBe('00:00:00') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(20) + + expect(results[0]).toMatchObject({ + start: '2023-06-11T21:00:00.000Z', + stop: '2023-06-11T22:00:00.000Z', + title: 'Word Of God' + }) + + expect(results[19]).toMatchObject({ + start: '2023-06-12T15:30:00.000Z', + stop: '2023-06-12T16:00:00.000Z', + title: 'La Suerte De Loli' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/compulms.com/compulms.com.config.js b/sites/compulms.com/compulms.com.config.js index c6202a48..3304a878 100644 --- a/sites/compulms.com/compulms.com.config.js +++ b/sites/compulms.com/compulms.com.config.js @@ -1,33 +1,33 @@ -const parser = require('epg-parser') - -module.exports = { - site: 'compulms.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml', - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title?.[0].value, - description: item.desc?.[0].value, - icon: item.icon?.[0], - start: item.start, - stop: item.stop - }) - }) - - return programs - } -} - -function parseItems(content, channel, date) { - const { programs } = parser.parse(content) - - return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) -} +const parser = require('epg-parser') + +module.exports = { + site: 'compulms.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: 'https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml', + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title?.[0].value, + description: item.desc?.[0].value, + icon: item.icon?.[0], + start: item.start, + stop: item.stop + }) + }) + + return programs + } +} + +function parseItems(content, channel, date) { + const { programs } = parser.parse(content) + + return programs.filter(p => p.channel === channel.site_id && date.isSame(p.start, 'day')) +} diff --git a/sites/compulms.com/compulms.com.test.js b/sites/compulms.com/compulms.com.test.js index 92c1fae5..4f63fde8 100644 --- a/sites/compulms.com/compulms.com.test.js +++ b/sites/compulms.com/compulms.com.test.js @@ -1,39 +1,39 @@ -// npm run grab -- --site=compulms.com - -const { parser, url } = require('./compulms.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'EnerGeek Retro', - xmltv_id: 'EnerGeekRetro.cl' -} - -it('can generate valid url', () => { - expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - let results = parser({ content, channel, date }) - - expect(results[0]).toMatchObject({ - start: '2022-11-29T03:00:00.000Z', - stop: '2022-11-29T03:30:00.000Z', - title: 'Noir', - description: - 'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir', - icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=compulms.com + +const { parser, url } = require('./compulms.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'EnerGeek Retro', + xmltv_id: 'EnerGeekRetro.cl' +} + +it('can generate valid url', () => { + expect(url).toBe('https://raw.githubusercontent.com/luisms123/tdt/master/guiaenergeek.xml') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + let results = parser({ content, channel, date }) + + expect(results[0]).toMatchObject({ + start: '2022-11-29T03:00:00.000Z', + stop: '2022-11-29T03:30:00.000Z', + title: 'Noir', + description: + 'Kirika Yuumura es una adolescente japonesa que no recuerda nada de su pasado, salvo la palabra NOIR, por lo que decidirá contactar con Mireille Bouquet, una asesina profesional para que la ayude a investigar. Ambas forman un equipo muy eficiente, que resuelve un trabajo tras otro con gran éxito, hasta que aparece un grupo conocido como "Les Soldats", relacionados con el pasado de Kirika. Estos tratarán de eliminar a las dos chicas, antes de que indaguen más hondo sobre la verdad acerca de Noir', + icon: 'https://pics.filmaffinity.com/nowaru_noir_tv_series-225888552-mmed.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/comteco.com.bo/comteco.com.bo.config.js b/sites/comteco.com.bo/comteco.com.bo.config.js index 04fbc5c8..2fbde814 100644 --- a/sites/comteco.com.bo/comteco.com.bo.config.js +++ b/sites/comteco.com.bo/comteco.com.bo.config.js @@ -1,68 +1,68 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'comteco.com.bo', - days: 2, - url: function ({ channel }) { - return `https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/${channel.site_id}` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: function ({ date }) { - const params = new URLSearchParams() - params.append('_method', 'POST') - params.append('fechaini', date.format('D/M/YYYY')) - params.append('fechafin', date.format('D/M/YYYY')) - - return params - } - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ title: parseTitle($item), start, stop }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('div > div.col-xs-11 > p > span').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'America/La_Paz') -} - -function parseTitle($item) { - return $item('div > div.col-xs-11 > p > strong').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#datosasociados > div > .list-group-item').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'comteco.com.bo', + days: 2, + url: function ({ channel }) { + return `https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/${channel.site_id}` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: function ({ date }) { + const params = new URLSearchParams() + params.append('_method', 'POST') + params.append('fechaini', date.format('D/M/YYYY')) + params.append('fechafin', date.format('D/M/YYYY')) + + return params + } + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ title: parseTitle($item), start, stop }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('div > div.col-xs-11 > p > span').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'America/La_Paz') +} + +function parseTitle($item) { + return $item('div > div.col-xs-11 > p > strong').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#datosasociados > div > .list-group-item').toArray() +} diff --git a/sites/comteco.com.bo/comteco.com.bo.test.js b/sites/comteco.com.bo/comteco.com.bo.test.js index c0514bf4..bdf7cfe0 100644 --- a/sites/comteco.com.bo/comteco.com.bo.test.js +++ b/sites/comteco.com.bo/comteco.com.bo.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=comteco.com.bo - -const { parser, url, request } = require('./comteco.com.bo.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ABYA YALA', - xmltv_id: 'AbyaYalaTV.bo' -} -const content = - '

    Canal Analógico: 48

    ABYA YALA

    00:00:00 Abya Yala noticias - 3ra edición

    01:00:00 Cierre de emisión

    23:00:00 Referentes

    Regresar a canales

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

    Canal Analógico: 48

    ABYA YALA

    00:00:00 Abya Yala noticias - 3ra edición

    01:00:00 Cierre de emisión

    23:00:00 Referentes

    Regresar a canales

    ' + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://comteco.com.bo/pages/canales-y-programacion-tv/paquete-oro/ABYA YALA' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date }) + expect(result.get('_method')).toBe('POST') + expect(result.get('fechaini')).toBe('25/11/2021') + expect(result.get('fechafin')).toBe('25/11/2021') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-25T04:00:00.000Z', + stop: '2021-11-25T05:00:00.000Z', + title: 'Abya Yala noticias - 3ra edición' + }, + { + start: '2021-11-25T05:00:00.000Z', + stop: '2021-11-26T03:00:00.000Z', + title: 'Cierre de emisión' + }, + { + start: '2021-11-26T03:00:00.000Z', + stop: '2021-11-26T03:30:00.000Z', + title: 'Referentes' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/cosmote.gr/cosmote.gr.config.js b/sites/cosmote.gr/cosmote.gr.config.js index 29ce29c4..d5fcc97c 100644 --- a/sites/cosmote.gr/cosmote.gr.config.js +++ b/sites/cosmote.gr/cosmote.gr.config.js @@ -1,78 +1,78 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'cosmote.gr', - days: 2, - url: function ({ date, channel }) { - return `https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=${date.format( - 'DD-MM-YYYY' - )}&_channelprogram_WAR_OTETVportlet_articleTitleUrl=${channel.site_id}` - }, - parser: function ({ date, content }) { - let programs = [] - const items = parseItems(content) - items.forEach((item, i) => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (i === 0 && start.hour > 12 && start.hour < 21) { - date = date.subtract(1, 'd') - start = start.minus({ days: 1 }) - } - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.channel_program-table--program > a').text() -} - -function parseCategory($item) { - const typeString = $item('.channel_program-table--program_type') - .children() - .remove() - .end() - .text() - .trim() - const [, category] = typeString.match(/\| (.*)/) || [null, null] - - return category -} - -function parseStart($item, date) { - const timeString = $item('span.start-time').text() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() -} - -function parseStop($item, date) { - const timeString = $item('span.end-time').text() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#_channelprogram_WAR_OTETVportlet_programs > tr.d-sm-table-row').toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'cosmote.gr', + days: 2, + url: function ({ date, channel }) { + return `https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=${date.format( + 'DD-MM-YYYY' + )}&_channelprogram_WAR_OTETVportlet_articleTitleUrl=${channel.site_id}` + }, + parser: function ({ date, content }) { + let programs = [] + const items = parseItems(content) + items.forEach((item, i) => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (i === 0 && start.hour > 12 && start.hour < 21) { + date = date.subtract(1, 'd') + start = start.minus({ days: 1 }) + } + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.channel_program-table--program > a').text() +} + +function parseCategory($item) { + const typeString = $item('.channel_program-table--program_type') + .children() + .remove() + .end() + .text() + .trim() + const [, category] = typeString.match(/\| (.*)/) || [null, null] + + return category +} + +function parseStart($item, date) { + const timeString = $item('span.start-time').text() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() +} + +function parseStop($item, date) { + const timeString = $item('span.end-time').text() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Athens' }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#_channelprogram_WAR_OTETVportlet_programs > tr.d-sm-table-row').toArray() +} diff --git a/sites/cosmote.gr/cosmote.gr.test.js b/sites/cosmote.gr/cosmote.gr.test.js index 5b818650..ab8e215f 100644 --- a/sites/cosmote.gr/cosmote.gr.test.js +++ b/sites/cosmote.gr/cosmote.gr.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=cosmote.gr - -const { parser, url } = require('./cosmote.gr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const date = dayjs.utc('2023-06-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '4e', - xmltv_id: '4E.gr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=08-06-2023&_channelprogram_WAR_OTETVportlet_articleTitleUrl=4e' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content1.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-07T20:30:00.000Z', - stop: '2023-06-07T21:45:00.000Z', - title: 'Τηλεφημερίδα', - category: 'Εκπομπή - Μαγκαζίνο' - }) - - expect(results[30]).toMatchObject({ - start: '2023-06-08T19:45:00.000Z', - stop: '2023-06-08T20:30:00.000Z', - title: 'Μικρό Απόδειπνο', - category: 'Special' - }) -}) - -it('can parse response when the guide starting before midnight', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content2.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-07T21:30:00.000Z', - stop: '2023-06-07T22:30:00.000Z', - title: 'Καλύτερα Αργά', - category: 'Ψυχαγωγική Εκπομπή' - }) - - expect(results[22]).toMatchObject({ - start: '2023-06-08T19:00:00.000Z', - stop: '2023-06-08T21:30:00.000Z', - title: 'Πίσω Από Τις Γραμμές', - category: 'Εκπομπή - Μαγκαζίνο' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=cosmote.gr + +const { parser, url } = require('./cosmote.gr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const date = dayjs.utc('2023-06-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '4e', + xmltv_id: '4E.gr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.cosmotetv.gr/portal/residential/program/epg/programchannel?p_p_id=channelprogram_WAR_OTETVportlet&p_p_lifecycle=0&_channelprogram_WAR_OTETVportlet_platform=IPTV&_channelprogram_WAR_OTETVportlet_date=08-06-2023&_channelprogram_WAR_OTETVportlet_articleTitleUrl=4e' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content1.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-07T20:30:00.000Z', + stop: '2023-06-07T21:45:00.000Z', + title: 'Τηλεφημερίδα', + category: 'Εκπομπή - Μαγκαζίνο' + }) + + expect(results[30]).toMatchObject({ + start: '2023-06-08T19:45:00.000Z', + stop: '2023-06-08T20:30:00.000Z', + title: 'Μικρό Απόδειπνο', + category: 'Special' + }) +}) + +it('can parse response when the guide starting before midnight', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content2.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-07T21:30:00.000Z', + stop: '2023-06-07T22:30:00.000Z', + title: 'Καλύτερα Αργά', + category: 'Ψυχαγωγική Εκπομπή' + }) + + expect(results[22]).toMatchObject({ + start: '2023-06-08T19:00:00.000Z', + stop: '2023-06-08T21:30:00.000Z', + title: 'Πίσω Από Τις Γραμμές', + category: 'Εκπομπή - Μαγκαζίνο' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/delta.nl/delta.nl.config.js b/sites/delta.nl/delta.nl.config.js index f37c26a9..0a9ebd28 100644 --- a/sites/delta.nl/delta.nl.config.js +++ b/sites/delta.nl/delta.nl.config.js @@ -1,70 +1,70 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'delta.nl', - days: 2, - url: function ({ channel, date }) { - return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date - .add(1, 'd') - .unix()}&includeDetails=true&channels=${channel.site_id}` - }, - async parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.title, - icon: item.images.thumbnail.url, - description: details.description, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - } - - return programs - }, - async channels() { - const items = await axios - .get('https://clientapi.tv.delta.nl/channels/list') - .then(r => r.data) - .catch(console.log) - - return items - .filter(i => i.type === 'TV') - .map(item => { - return { - lang: 'nl', - site_id: item['ID'], - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.ID) return {} - const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.start) -} - -function parseStop(item) { - return dayjs.unix(item.end) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - - return data[channel.site_id] || [] -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'delta.nl', + days: 2, + url: function ({ channel, date }) { + return `https://clientapi.tv.delta.nl/guide/channels/list?start=${date.unix()}&end=${date + .add(1, 'd') + .unix()}&includeDetails=true&channels=${channel.site_id}` + }, + async parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.title, + icon: item.images.thumbnail.url, + description: details.description, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + } + + return programs + }, + async channels() { + const items = await axios + .get('https://clientapi.tv.delta.nl/channels/list') + .then(r => r.data) + .catch(console.log) + + return items + .filter(i => i.type === 'TV') + .map(item => { + return { + lang: 'nl', + site_id: item['ID'], + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.ID) return {} + const url = `https://clientapi.tv.delta.nl/guide/4/details/${item.ID}?X-Response-Version=4.5` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.start) +} + +function parseStop(item) { + return dayjs.unix(item.end) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + + return data[channel.site_id] || [] +} diff --git a/sites/delta.nl/delta.nl.test.js b/sites/delta.nl/delta.nl.test.js index c4a49af9..e7467d24 100644 --- a/sites/delta.nl/delta.nl.test.js +++ b/sites/delta.nl/delta.nl.test.js @@ -1,70 +1,70 @@ -// npm run channels:parse -- --config=./sites/delta.nl/delta.nl.config.js --output=./sites/delta.nl/delta.nl.channels.xml -// npm run grab -- --site=delta.nl - -const { parser, url } = require('./delta.nl.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1', - xmltv_id: 'NPO1.nl' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1' - ) -}) - -it('can parse response', done => { - axios.get.mockImplementation(() => - Promise.resolve({ - data: JSON.parse( - '{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}' - ) - }) - ) - - const content = - '{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}' - - parser({ date, channel, content }) - .then(result => { - expect(result).toMatchObject([ - { - start: '2021-11-11T23:56:00.000Z', - stop: '2021-11-12T00:22:00.000Z', - title: 'NOS Journaal', - description: - 'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.', - icon: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg' - } - ]) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: '{"code":500,"message":"Error retrieving guide"}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(error => { - done(error) - }) -}) +// npm run channels:parse -- --config=./sites/delta.nl/delta.nl.config.js --output=./sites/delta.nl/delta.nl.channels.xml +// npm run grab -- --site=delta.nl + +const { parser, url } = require('./delta.nl.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2021-11-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1', + xmltv_id: 'NPO1.nl' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://clientapi.tv.delta.nl/guide/channels/list?start=1636675200&end=1636761600&includeDetails=true&channels=1' + ) +}) + +it('can parse response', done => { + axios.get.mockImplementation(() => + Promise.resolve({ + data: JSON.parse( + '{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"Eigen Huis & Tuin: Lekker Leven","description":"Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/b291/561946.jpg"}},"additionalInformation":{"metadataID":"M~c512c206-95e5-11ec-87d8-494f70130311","externalMetadataID":"E~RTL4-89d99356_6599_4b65_a7a0_a93f39019645"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}' + ) + }) + ) + + const content = + '{"1":[{"ID":"P~945cb98e-3d19-11ec-8456-953363d7a344","seriesID":"S~d37c4626-b691-11ea-ba69-255835135f02","channelID":"1","start":1636674960,"end":1636676520,"catchupAvailableUntil":1637279760,"title":"NOS Journaal","images":{"thumbnail":{"url":"https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg"}},"additionalInformation":{"metadataID":"M~944f3c6e-3d19-11ec-9faf-2735f2e98d2a","externalMetadataID":"E~TV01-2026117420668"},"parentalGuidance":{"kijkwijzer":["AL"]},"restrictions":{"startoverDisabled":false,"catchupDisabled":false,"recordingDisabled":false},"isFiller":false}]}' + + parser({ date, channel, content }) + .then(result => { + expect(result).toMatchObject([ + { + start: '2021-11-11T23:56:00.000Z', + stop: '2021-11-12T00:22:00.000Z', + title: 'NOS Journaal', + description: + 'Nederlands lifestyleprogramma uit 2022 (ook in HD) met dagelijkse inspiratie voor een lekker leven in en om het huis.\nPresentatrice Froukje de Both, kok Hugo Kennis en een team van experts, onder wie tuinman Tom Groot, geven praktische tips op het gebied van wonen, lifestyle, tuinieren en koken. Daarmee kun je zelf direct aan de slag om je leven leuker én gezonder te maken. Afl. 15 van seizoen 4.', + icon: 'https://cdn.gvidi.tv/img/booxmedia/e19c/static/NOS%20Journaal5.jpg' + } + ]) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: '{"code":500,"message":"Error retrieving guide"}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(error => { + done(error) + }) +}) diff --git a/sites/digiturk.com.tr/digiturk.com.tr.config.js b/sites/digiturk.com.tr/digiturk.com.tr.config.js index 670fb569..694cbd91 100644 --- a/sites/digiturk.com.tr/digiturk.com.tr.config.js +++ b/sites/digiturk.com.tr/digiturk.com.tr.config.js @@ -1,77 +1,77 @@ -const _ = require('lodash') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) -// category list is not complete -// const categories = { -// '00': 'Diğer', -// E0: 'Romantik Komedi', -// E1: 'Aksiyon', -// E4: 'Macera', -// E5: 'Dram', -// E6: 'Fantastik', -// E7: 'Komedi', -// E8: 'Korku', -// EB: 'Polisiye', -// EF: 'Western', -// FA: 'Macera', -// FB: 'Yarışma', -// FC: 'Eğlence', -// F0: 'Reality-Show', -// F2: 'Haberler', -// F4: 'Belgesel', -// F6: 'Eğitim', -// F7: 'Sanat ve Kültür', -// F9: 'Life Style' -// } - -module.exports = { - site: 'digiturk.com.tr', - days: 2, - url: function ({ date, channel }) { - return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${ - channel.site_id - }&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false` - }, - request: { - method: 'GET', - headers: { - Referer: 'https://www.digiturk.com.tr/' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.PName, - // description: item.LongDescription, - // category: parseCategory(item), - start: parseTime(item.PStartTime), - stop: parseTime(item.PEndTime) - }) - }) - - programs = _.sortBy(programs, 'start') - - return programs - } -} - -function parseTime(time) { - let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', '')) - return dayjs(timestamp) -} - -// function parseCategory(item) { -// return (item.PGenre) ? categories[item.PGenre] : null -// } - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - return data && data.BChannels && data.BChannels[0].CPrograms ? data.BChannels[0].CPrograms : [] -} +const _ = require('lodash') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) +// category list is not complete +// const categories = { +// '00': 'Diğer', +// E0: 'Romantik Komedi', +// E1: 'Aksiyon', +// E4: 'Macera', +// E5: 'Dram', +// E6: 'Fantastik', +// E7: 'Komedi', +// E8: 'Korku', +// EB: 'Polisiye', +// EF: 'Western', +// FA: 'Macera', +// FB: 'Yarışma', +// FC: 'Eğlence', +// F0: 'Reality-Show', +// F2: 'Haberler', +// F4: 'Belgesel', +// F6: 'Eğitim', +// F7: 'Sanat ve Kültür', +// F9: 'Life Style' +// } + +module.exports = { + site: 'digiturk.com.tr', + days: 2, + url: function ({ date, channel }) { + return `https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=${ + channel.site_id + }&date=${date.format('DD.MM.YYYY')}&tomorrow=false&primetime=false` + }, + request: { + method: 'GET', + headers: { + Referer: 'https://www.digiturk.com.tr/' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.PName, + // description: item.LongDescription, + // category: parseCategory(item), + start: parseTime(item.PStartTime), + stop: parseTime(item.PEndTime) + }) + }) + + programs = _.sortBy(programs, 'start') + + return programs + } +} + +function parseTime(time) { + let timestamp = parseInt(time.replace('/Date(', '').replace('+0300)/', '')) + return dayjs(timestamp) +} + +// function parseCategory(item) { +// return (item.PGenre) ? categories[item.PGenre] : null +// } + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + return data && data.BChannels && data.BChannels[0].CPrograms ? data.BChannels[0].CPrograms : [] +} diff --git a/sites/digiturk.com.tr/digiturk.com.tr.test.js b/sites/digiturk.com.tr/digiturk.com.tr.test.js index 61f6c06f..3e24acab 100644 --- a/sites/digiturk.com.tr/digiturk.com.tr.test.js +++ b/sites/digiturk.com.tr/digiturk.com.tr.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=digiturk.com.tr - -const { parser, url } = require('./digiturk.com.tr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '14', - xmltv_id: 'beINMovies2Action.qa' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-18T20:40:00.000Z', - stop: '2023-01-18T22:32:00.000Z', - title: 'PARÇALANMIŞ' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-19T05:04:00.000Z', - stop: '2023-01-19T06:42:00.000Z', - title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=digiturk.com.tr + +const { parser, url } = require('./digiturk.com.tr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '14', + xmltv_id: 'beINMovies2Action.qa' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.digiturk.com.tr/_Ajax/getBroadcast.aspx?channelNo=14&date=19.01.2023&tomorrow=false&primetime=false' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-18T20:40:00.000Z', + stop: '2023-01-18T22:32:00.000Z', + title: 'PARÇALANMIŞ' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-19T05:04:00.000Z', + stop: '2023-01-19T06:42:00.000Z', + title: 'HIZLI VE ÖFKELİ: TOKYO YARIŞI' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com.ar/directv.com.ar.config.js b/sites/directv.com.ar/directv.com.ar.config.js index 9918e291..a483cbb2 100644 --- a/sites/directv.com.ar/directv.com.ar.config.js +++ b/sites/directv.com.ar/directv.com.ar.config.js @@ -1,100 +1,100 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'directv.com.ar', - days: 2, - url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming', - request: { - method: 'POST', - headers: { - Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;', - Accept: '*/*', - 'Accept-Language': 'es-419,es;q=0.9', - Connection: 'keep-alive', - 'Content-Type': 'application/json; charset=UTF-8', - Origin: 'https://www.directv.com.ar', - Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', - 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"Windows"' - }, - data({ channel, date }) { - const [channelNum, channelName] = channel.site_id.split('#') - - return { - filterParameters: { - day: date.date(), - time: 0, - minute: 0, - month: date.month() + 1, - year: date.year(), - offSetValue: 0, - homeScreenFilter: '', - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum, - channelName: channelName.replace('&', '&') - } - } - } - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - rating: parseRating(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} - -function parseStart(item) { - return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') -} - -function parseStop(item) { - return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') -} - -function parseItems(content, channel) { - if (!content) return [] - let [ChannelNumber, ChannelName] = channel.site_id.split('#') - ChannelName = ChannelName.replace('&', '&') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.d)) return [] - const channelData = data.d.find( - c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName - ) - - return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'directv.com.ar', + days: 2, + url: 'https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming', + request: { + method: 'POST', + headers: { + Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;', + Accept: '*/*', + 'Accept-Language': 'es-419,es;q=0.9', + Connection: 'keep-alive', + 'Content-Type': 'application/json; charset=UTF-8', + Origin: 'https://www.directv.com.ar', + Referer: 'https://www.directv.com.ar/guia/ChannelDetail.aspx?id=1740&name=TLCHD', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"' + }, + data({ channel, date }) { + const [channelNum, channelName] = channel.site_id.split('#') + + return { + filterParameters: { + day: date.date(), + time: 0, + minute: 0, + month: date.month() + 1, + year: date.year(), + offSetValue: 0, + homeScreenFilter: '', + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum, + channelName: channelName.replace('&', '&') + } + } + } + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + rating: parseRating(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} + +function parseStart(item) { + return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') +} + +function parseStop(item) { + return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Argentina/Buenos_Aires') +} + +function parseItems(content, channel) { + if (!content) return [] + let [ChannelNumber, ChannelName] = channel.site_id.split('#') + ChannelName = ChannelName.replace('&', '&') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.d)) return [] + const channelData = data.d.find( + c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName + ) + + return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] +} diff --git a/sites/directv.com.ar/directv.com.ar.test.js b/sites/directv.com.ar/directv.com.ar.test.js index 95aa923b..d5c1a7db 100644 --- a/sites/directv.com.ar/directv.com.ar.test.js +++ b/sites/directv.com.ar/directv.com.ar.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=directv.com.ar - -const { parser, url, request } = require('./directv.com.ar.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-06-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '207#A&EHD', - xmltv_id: 'AEHDSouth.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - filterParameters: { - day: 19, - time: 0, - minute: 0, - month: 6, - year: 2022, - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum: '207', - channelName: 'A&EHD' - } - }) -}) - -it('can parse response', () => { - const content = - '{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-19T03:00:00.000Z', - stop: '2022-06-19T03:15:00.000Z', - title: 'Chicas guapas', - description: - 'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.', - rating: { - system: 'MPA', - value: 'NR' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=directv.com.ar + +const { parser, url, request } = require('./directv.com.ar.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-06-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '207#A&EHD', + xmltv_id: 'AEHDSouth.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.directv.com.ar/guia/ChannelDetail.aspx/GetProgramming') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16; PGLang=S; PGCulture=es-AR;' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + filterParameters: { + day: 19, + time: 0, + minute: 0, + month: 6, + year: 2022, + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum: '207', + channelName: 'A&EHD' + } + }) +}) + +it('can parse response', () => { + const content = + '{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-19T03:00:00.000Z', + stop: '2022-06-19T03:15:00.000Z', + title: 'Chicas guapas', + description: + 'Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.', + rating: { + system: 'MPA', + value: 'NR' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com.uy/directv.com.uy.config.js b/sites/directv.com.uy/directv.com.uy.config.js index f1a828ac..c071406c 100644 --- a/sites/directv.com.uy/directv.com.uy.config.js +++ b/sites/directv.com.uy/directv.com.uy.config.js @@ -1,85 +1,85 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'directv.com.uy', - days: 2, - url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' - }, - data({ channel, date }) { - const [channelNum, channelName] = channel.site_id.split('#') - - return { - filterParameters: { - day: date.date(), - time: 0, - minute: 0, - month: date.month() + 1, - year: date.year(), - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum, - channelName: channelName.replace('&', '&') - } - } - } - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - rating: parseRating(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} - -function parseStart(item) { - return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') -} - -function parseStop(item) { - return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') -} - -function parseItems(content, channel) { - if (!content) return [] - let [ChannelNumber, ChannelName] = channel.site_id.split('#') - ChannelName = ChannelName.replace('&', '&') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.d)) return [] - const channelData = data.d.find( - c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName - ) - - return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'directv.com.uy', + days: 2, + url: 'https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' + }, + data({ channel, date }) { + const [channelNum, channelName] = channel.site_id.split('#') + + return { + filterParameters: { + day: date.date(), + time: 0, + minute: 0, + month: date.month() + 1, + year: date.year(), + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum, + channelName: channelName.replace('&', '&') + } + } + } + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + rating: parseRating(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} + +function parseStart(item) { + return dayjs.tz(item.startTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') +} + +function parseStop(item) { + return dayjs.tz(item.endTimeString, 'M/D/YYYY h:mm:ss A', 'America/Montevideo') +} + +function parseItems(content, channel) { + if (!content) return [] + let [ChannelNumber, ChannelName] = channel.site_id.split('#') + ChannelName = ChannelName.replace('&', '&') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.d)) return [] + const channelData = data.d.find( + c => c.ChannelNumber == ChannelNumber && c.ChannelName === ChannelName + ) + + return channelData && Array.isArray(channelData.ProgramList) ? channelData.ProgramList : [] +} diff --git a/sites/directv.com.uy/directv.com.uy.test.js b/sites/directv.com.uy/directv.com.uy.test.js index 146748e2..fa316c95 100644 --- a/sites/directv.com.uy/directv.com.uy.test.js +++ b/sites/directv.com.uy/directv.com.uy.test.js @@ -1,78 +1,78 @@ -// npm run grab -- --site=directv.com.uy - -const { parser, url, request } = require('./directv.com.uy.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '184#VTV', - xmltv_id: 'VTV.uy' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - filterParameters: { - day: 29, - time: 0, - minute: 0, - month: 8, - year: 2022, - offSetValue: 0, - filtersScreenFilters: [''], - isHd: '', - isChannelDetails: 'Y', - channelNum: '184', - channelName: 'VTV' - } - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-29T03:00:00.000Z', - stop: '2022-08-29T05:00:00.000Z', - title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio', - description: - 'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).', - rating: { - system: 'MPA', - value: 'NR' - } - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=directv.com.uy + +const { parser, url, request } = require('./directv.com.uy.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '184#VTV', + xmltv_id: 'VTV.uy' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.directv.com.uy/guia/ChannelDetail.aspx/GetProgramming') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Cookie: 'PGCSS=16384; PGLang=S; PGCulture=es-UY;' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + filterParameters: { + day: 29, + time: 0, + minute: 0, + month: 8, + year: 2022, + offSetValue: 0, + filtersScreenFilters: [''], + isHd: '', + isChannelDetails: 'Y', + channelNum: '184', + channelName: 'VTV' + } + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-29T03:00:00.000Z', + stop: '2022-08-29T05:00:00.000Z', + title: 'Peñarol vs. Danubio : Fútbol Uruguayo Primera División - Peñarol vs. Danubio', + description: + 'Jornada 5 del Torneo Clausura 2022. Peñarol recibe a Danubio en el estadio Campeón del Siglo. Los carboneros llevan 3 partidos sin caer (2PG 1PE), mientras que los franjeados acumulan 6 juegos sin derrotas (4PG 2PE).', + rating: { + system: 'MPA', + value: 'NR' + } + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/directv.com/directv.com.config.js b/sites/directv.com/directv.com.config.js index 3346a06c..c048f27c 100644 --- a/sites/directv.com/directv.com.config.js +++ b/sites/directv.com/directv.com.config.js @@ -1,113 +1,113 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'directv.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - headers: { - 'Accept-Language': 'en-US,en;q=0.5', - Connection: 'keep-alive' - } - }, - url({ date, channel }) { - const [channelId, childId] = channel.site_id.split('#') - return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` - }, - async parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - if (item.programID === '-1') continue - const detail = await loadProgramDetail(item.programID) - const start = parseStart(item) - const stop = start.add(item.duration, 'm') - programs.push({ - title: item.title, - sub_title: item.episodeTitle, - description: parseDescription(detail), - rating: parseRating(item), - date: parseYear(detail), - category: item.subcategoryList, - season: item.seasonNumber, - episode: item.episodeNumber, - icon: parseIcon(item), - start, - stop - }) - } - - return programs - }, - async channels({ zip }) { - const html = await axios - .get('https://www.directv.com/guide', { - headers: { - cookie: `dtve-prospect-zip=${zip}` - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const script = $('#dtvClientData').html() - const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] - const data = JSON.parse(json) - - let items = data.guideData.channels - - return items.map(item => { - return { - lang: 'en', - site_id: item.chNum, - name: item.chName - } - }) - } -} - -function parseDescription(detail) { - return detail ? detail.description : null -} -function parseYear(detail) { - return detail ? detail.releaseYear : null -} -function parseRating(item) { - return item.rating - ? { - system: 'MPA', - value: item.rating - } - : null -} -function parseIcon(item) { - return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null -} -function loadProgramDetail(programID) { - return axios - .get(`https://www.directv.com/json/program/flip/${programID}`) - .then(r => r.data) - .then(d => d.programDetail) - .catch(console.err) -} - -function parseStart(item) { - return dayjs.utc(item.airTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data.schedule)) return [] - - const [, childId] = channel.site_id.split('#') - const channelData = data.schedule.find(i => i.chId == childId) - return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'directv.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + headers: { + 'Accept-Language': 'en-US,en;q=0.5', + Connection: 'keep-alive' + } + }, + url({ date, channel }) { + const [channelId, childId] = channel.site_id.split('#') + return `https://www.directv.com/json/channelschedule?channels=${channelId}&startTime=${date.format()}&hours=24&chId=${childId}` + }, + async parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + if (item.programID === '-1') continue + const detail = await loadProgramDetail(item.programID) + const start = parseStart(item) + const stop = start.add(item.duration, 'm') + programs.push({ + title: item.title, + sub_title: item.episodeTitle, + description: parseDescription(detail), + rating: parseRating(item), + date: parseYear(detail), + category: item.subcategoryList, + season: item.seasonNumber, + episode: item.episodeNumber, + icon: parseIcon(item), + start, + stop + }) + } + + return programs + }, + async channels({ zip }) { + const html = await axios + .get('https://www.directv.com/guide', { + headers: { + cookie: `dtve-prospect-zip=${zip}` + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const script = $('#dtvClientData').html() + const [, json] = script.match(/var dtvClientData = (.*);/) || [null, null] + const data = JSON.parse(json) + + let items = data.guideData.channels + + return items.map(item => { + return { + lang: 'en', + site_id: item.chNum, + name: item.chName + } + }) + } +} + +function parseDescription(detail) { + return detail ? detail.description : null +} +function parseYear(detail) { + return detail ? detail.releaseYear : null +} +function parseRating(item) { + return item.rating + ? { + system: 'MPA', + value: item.rating + } + : null +} +function parseIcon(item) { + return item.primaryImageUrl ? `https://www.directv.com${item.primaryImageUrl}` : null +} +function loadProgramDetail(programID) { + return axios + .get(`https://www.directv.com/json/program/flip/${programID}`) + .then(r => r.data) + .then(d => d.programDetail) + .catch(console.err) +} + +function parseStart(item) { + return dayjs.utc(item.airTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data.schedule)) return [] + + const [, childId] = channel.site_id.split('#') + const channelData = data.schedule.find(i => i.chId == childId) + return channelData.schedules && Array.isArray(channelData.schedules) ? channelData.schedules : [] +} diff --git a/sites/directv.com/directv.com.test.js b/sites/directv.com/directv.com.test.js index d84d8029..0b201fc0 100644 --- a/sites/directv.com/directv.com.test.js +++ b/sites/directv.com/directv.com.test.js @@ -1,98 +1,98 @@ -// node ./scripts/commands/parse-channels.js --config=./sites/directv.com/directv.com.config.js --output=./sites/directv.com/directv.com.channels.xml --set=zip:10001 -// npm run grab -- --site=directv.com - -const { parser, url } = require('./directv.com.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '249#249', - xmltv_id: 'ComedyCentralEast.us' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' - ) -}) - -it('can parse response', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-01-14T23:00:00.000Z', - stop: '2023-01-15T01:00:00.000Z', - title: 'Men in Black II', - description: - 'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.', - date: '2002', - icon: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', - category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], - rating: { - system: 'MPA', - value: 'TV14' - } - }, - { - start: '2023-01-15T06:00:00.000Z', - stop: '2023-01-15T06:30:00.000Z', - title: 'South Park', - sub_title: 'Goth Kids 3: Dawn of the Posers', - description: 'The goth kids are sent to a camp for troubled children.', - icon: 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg', - category: ['Series', 'Animation', 'Comedy'], - season: 17, - episode: 4, - rating: { - system: 'MPA', - value: 'TVMA' - } - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) - parser({ content, channel }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// node ./scripts/commands/parse-channels.js --config=./sites/directv.com/directv.com.config.js --output=./sites/directv.com/directv.com.channels.xml --set=zip:10001 +// npm run grab -- --site=directv.com + +const { parser, url } = require('./directv.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '249#249', + xmltv_id: 'ComedyCentralEast.us' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.directv.com/json/channelschedule?channels=249&startTime=2023-01-15T00:00:00Z&hours=24&chId=249' + ) +}) + +it('can parse response', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if (url === 'https://www.directv.com/json/program/flip/MV001173520000') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://www.directv.com/json/program/flip/EP002298270445') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-01-14T23:00:00.000Z', + stop: '2023-01-15T01:00:00.000Z', + title: 'Men in Black II', + description: + 'Kay (Tommy Lee Jones) and Jay (Will Smith) reunite to provide our best line of defense against a seductress who levels the toughest challenge yet to the MIBs mission statement: protecting the earth from the scum of the universe. While investigating a routine crime, Jay uncovers a plot masterminded by Serleena (Boyle), a Kylothian monster who disguises herself as a lingerie model. When Serleena takes the MIB building hostage, there is only one person Jay can turn to -- his former MIB partner.', + date: '2002', + icon: 'https://www.directv.com/db_photos/movies/AllPhotosAPGI/29160/29160_aa.jpg', + category: ['Comedy', 'Movies Anywhere', 'Action/Adventure', 'Science Fiction'], + rating: { + system: 'MPA', + value: 'TV14' + } + }, + { + start: '2023-01-15T06:00:00.000Z', + stop: '2023-01-15T06:30:00.000Z', + title: 'South Park', + sub_title: 'Goth Kids 3: Dawn of the Posers', + description: 'The goth kids are sent to a camp for troubled children.', + icon: 'https://www.directv.com/db_photos/showcards/v5/AllPhotos/184338/p184338_b_v5_aa.jpg', + category: ['Series', 'Animation', 'Comedy'], + season: 17, + episode: 4, + rating: { + system: 'MPA', + value: 'TVMA' + } + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) + parser({ content, channel }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/dishtv.in/dishtv.in.config.js b/sites/dishtv.in/dishtv.in.config.js index 1df15998..cab65646 100644 --- a/sites/dishtv.in/dishtv.in.config.js +++ b/sites/dishtv.in/dishtv.in.config.js @@ -1,145 +1,145 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'dishtv.in', - days: 2, - url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', - request: { - method: 'POST', - data({ channel, date }) { - return { - Channelarr: channel.site_id, - fromdate: date.format('YYYYMMDDHHmm'), - todate: date.add(1, 'd').format('YYYYMMDDHHmm') - } - } - }, - parser: function ({ content, date }) { - let programs = [] - const data = parseContent(content) - const items = parseItems(data) - items.forEach(item => { - const title = parseTitle(item) - const start = parseStart(item, date) - const stop = parseStop(item, start) - if (title === 'No Information Available') return - - programs.push({ - title, - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - }, - async channels() { - const channelguide = await axios - .get('https://www.dishtv.in/channelguide/') - .then(r => r.data) - .catch(console.log) - const $channelguide = cheerio.load(channelguide) - - let ids = [] - $channelguide('#MainContent_recordPagging li').each((i, item) => { - const onclick = $channelguide(item).find('a').attr('onclick') - const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null] - - ids = ids.concat(list.split(',')) - }) - ids = ids.filter(Boolean) - - const channels = {} - const channelList = await axios - .post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', { - strChannel: '' - }) - .then(r => r.data) - .catch(console.log) - const $channelList = cheerio.load(channelList.d) - $channelList('#tblpackChnl > div').each((i, item) => { - let num = $channelList(item).find('p:nth-child(2)').text().trim() - const name = $channelList(item).find('p').first().text().trim() - - if (num === '') return - - channels[parseInt(num)] = { - name - } - }) - - const date = dayjs().add(1, 'd') - const promises = [] - for (let id of ids) { - const promise = axios - .post( - 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', - { - Channelarr: id, - fromdate: date.format('YYYYMMDD[0000]'), - todate: date.format('YYYYMMDD[2300]') - }, - { timeout: 5000 } - ) - .then(r => r.data) - .then(data => { - const $channelGuide = cheerio.load(data.d) - - const num = $channelGuide('.cnl-fav > a > span').text().trim() - - if (channels[num]) { - channels[num].site_id = id - } - }) - .catch(console.log) - - promises.push(promise) - } - - await Promise.allSettled(promises) - - return Object.values(channels) - } -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('a').text() -} - -function parseStart(item) { - const $ = cheerio.load(item) - const onclick = $('i.fa-circle').attr('onclick') - const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/) - - return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata') -} - -function parseStop(item, start) { - const $ = cheerio.load(item) - const duration = $('*').data('time') - - return start.add(duration, 'm') -} - -function parseContent(content) { - const data = JSON.parse(content) - - return data.d -} - -function parseItems(data) { - const $ = cheerio.load(data) - - return $('.datatime').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'dishtv.in', + days: 2, + url: 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', + request: { + method: 'POST', + data({ channel, date }) { + return { + Channelarr: channel.site_id, + fromdate: date.format('YYYYMMDDHHmm'), + todate: date.add(1, 'd').format('YYYYMMDDHHmm') + } + } + }, + parser: function ({ content, date }) { + let programs = [] + const data = parseContent(content) + const items = parseItems(data) + items.forEach(item => { + const title = parseTitle(item) + const start = parseStart(item, date) + const stop = parseStop(item, start) + if (title === 'No Information Available') return + + programs.push({ + title, + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + }, + async channels() { + const channelguide = await axios + .get('https://www.dishtv.in/channelguide/') + .then(r => r.data) + .catch(console.log) + const $channelguide = cheerio.load(channelguide) + + let ids = [] + $channelguide('#MainContent_recordPagging li').each((i, item) => { + const onclick = $channelguide(item).find('a').attr('onclick') + const [, list] = onclick.match(/ShowNextPageResult\('([^']+)/) || [null, null] + + ids = ids.concat(list.split(',')) + }) + ids = ids.filter(Boolean) + + const channels = {} + const channelList = await axios + .post('https://www.dishtv.in/WebServiceMethod.aspx/GetChannelListFromMobileAPI', { + strChannel: '' + }) + .then(r => r.data) + .catch(console.log) + const $channelList = cheerio.load(channelList.d) + $channelList('#tblpackChnl > div').each((i, item) => { + let num = $channelList(item).find('p:nth-child(2)').text().trim() + const name = $channelList(item).find('p').first().text().trim() + + if (num === '') return + + channels[parseInt(num)] = { + name + } + }) + + const date = dayjs().add(1, 'd') + const promises = [] + for (let id of ids) { + const promise = axios + .post( + 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram', + { + Channelarr: id, + fromdate: date.format('YYYYMMDD[0000]'), + todate: date.format('YYYYMMDD[2300]') + }, + { timeout: 5000 } + ) + .then(r => r.data) + .then(data => { + const $channelGuide = cheerio.load(data.d) + + const num = $channelGuide('.cnl-fav > a > span').text().trim() + + if (channels[num]) { + channels[num].site_id = id + } + }) + .catch(console.log) + + promises.push(promise) + } + + await Promise.allSettled(promises) + + return Object.values(channels) + } +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('a').text() +} + +function parseStart(item) { + const $ = cheerio.load(item) + const onclick = $('i.fa-circle').attr('onclick') + const [, time] = onclick.match(/RecordingEnteryOpen\('.*','.*','(.*)','.*',.*\)/) + + return dayjs.tz(time, 'YYYYMMDDHHmm', 'Asia/Kolkata') +} + +function parseStop(item, start) { + const $ = cheerio.load(item) + const duration = $('*').data('time') + + return start.add(duration, 'm') +} + +function parseContent(content) { + const data = JSON.parse(content) + + return data.d +} + +function parseItems(data) { + const $ = cheerio.load(data) + + return $('.datatime').toArray() +} diff --git a/sites/dishtv.in/dishtv.in.test.js b/sites/dishtv.in/dishtv.in.test.js index 124ffd56..74ed3861 100644 --- a/sites/dishtv.in/dishtv.in.test.js +++ b/sites/dishtv.in/dishtv.in.test.js @@ -1,45 +1,45 @@ -// npm run channels:parse -- --config=./sites/dishtv.in/dishtv.in.config.js --output=./sites/dishtv.in/dishtv.in.channels.xml -// npm run grab -- --site=dishtv.in - -const { parser, url, request } = require('./dishtv.in.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' } -const content = - '{"d":"\\u003cdiv class=\\"pgrid\\"\\u003e\\u003cdiv class=\\"img sm-30 grid\\"\\u003e\\u003cimg class=\\"chnl-logo\\" src=\\"http://imagesdishtvd2h.whatsonindia.com/dasimages/channel/landscape/360x270/hiyj8ndf.png\\" onclick=\\"ShowChannelGuid(\\u0027womens-active\\u0027,\\u002710000000075992337\\u0027);\\" /\\u003e\\u003cdiv class=\\"cnl-fav\\"\\u003e\\u003ca href=\\"javascript:;\\"\\u003e\\u003cem\\u003ech. no\\u003c/em\\u003e\\u003cspan\\u003e117\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/div\\u003e\\u003ci class=\\"fa fa-heart Set_Favourite_Channel\\" aria-hidden=\\"true\\" title=\\"Set womens active channel as your favourite channel\\" onclick=\\"SetFavouriteChannel();\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003cdiv class=\\"grid-wrap\\"\\u003e\\u003cdiv class=\\"sm-30 grid datatime\\" data-time=\\"24\\" data-starttime=\\"12:00 AM\\" data-endttime=\\"12:24 AM\\" data-reamintime=\\"0\\"\\u003e\\u003ca title=\\"Event Name: Cynthia Williams - Diwali Look Part 01\\r\\nStart Time: 12:00 AM\\r\\nDuration: 24min\\r\\nSynopsis: Learn diwali look by cynthia williams p1\\r\\n\\" href=\\"javascript:;\\" onclick=\\"ShowCurrentTime(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111051200\\u0027)\\"\\u003eCynthia Williams - Diwali Look Part 01\\u003c/a\\u003e\\u003cdiv class=\\"cnlSerialIcon\\"\\u003e\\u003ci class=\\"fa fa-heart\\" aria-hidden=\\"true\\" title=\\"Set Favourite Serial\\" onclick=\\"SetFavouriteShow();\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-clock-o\\" aria-hidden=\\"true\\" title=\\"Reminder Serial\\" onclick=\\"ReminderEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027)\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-circle\\" aria-hidden=\\"true\\" title=\\"Record Serial\\" onclick=\\"RecordingEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027,30000000550913679)\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e"}' - -it('can generate valid url', () => { - expect(url).toBe( - 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram' - ) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result).toMatchObject({ - Channelarr: '10000000075992337', - fromdate: '202111050000', - todate: '202111060000' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: 'Thu, 04 Nov 2021 18:30:00 GMT', - stop: 'Thu, 04 Nov 2021 18:54:00 GMT', - title: 'Cynthia Williams - Diwali Look Part 01' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{"d":""}' }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/dishtv.in/dishtv.in.config.js --output=./sites/dishtv.in/dishtv.in.channels.xml +// npm run grab -- --site=dishtv.in + +const { parser, url, request } = require('./dishtv.in.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '10000000075992337', xmltv_id: 'WomensActive.in' } +const content = + '{"d":"\\u003cdiv class=\\"pgrid\\"\\u003e\\u003cdiv class=\\"img sm-30 grid\\"\\u003e\\u003cimg class=\\"chnl-logo\\" src=\\"http://imagesdishtvd2h.whatsonindia.com/dasimages/channel/landscape/360x270/hiyj8ndf.png\\" onclick=\\"ShowChannelGuid(\\u0027womens-active\\u0027,\\u002710000000075992337\\u0027);\\" /\\u003e\\u003cdiv class=\\"cnl-fav\\"\\u003e\\u003ca href=\\"javascript:;\\"\\u003e\\u003cem\\u003ech. no\\u003c/em\\u003e\\u003cspan\\u003e117\\u003c/span\\u003e\\u003c/a\\u003e\\u003c/div\\u003e\\u003ci class=\\"fa fa-heart Set_Favourite_Channel\\" aria-hidden=\\"true\\" title=\\"Set womens active channel as your favourite channel\\" onclick=\\"SetFavouriteChannel();\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003cdiv class=\\"grid-wrap\\"\\u003e\\u003cdiv class=\\"sm-30 grid datatime\\" data-time=\\"24\\" data-starttime=\\"12:00 AM\\" data-endttime=\\"12:24 AM\\" data-reamintime=\\"0\\"\\u003e\\u003ca title=\\"Event Name: Cynthia Williams - Diwali Look Part 01\\r\\nStart Time: 12:00 AM\\r\\nDuration: 24min\\r\\nSynopsis: Learn diwali look by cynthia williams p1\\r\\n\\" href=\\"javascript:;\\" onclick=\\"ShowCurrentTime(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111051200\\u0027)\\"\\u003eCynthia Williams - Diwali Look Part 01\\u003c/a\\u003e\\u003cdiv class=\\"cnlSerialIcon\\"\\u003e\\u003ci class=\\"fa fa-heart\\" aria-hidden=\\"true\\" title=\\"Set Favourite Serial\\" onclick=\\"SetFavouriteShow();\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-clock-o\\" aria-hidden=\\"true\\" title=\\"Reminder Serial\\" onclick=\\"ReminderEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027)\\"\\u003e\\u003c/i\\u003e\\u003ci class=\\"fa fa-circle\\" aria-hidden=\\"true\\" title=\\"Record Serial\\" onclick=\\"RecordingEnteryOpen(\\u002730000000550913679\\u0027,\\u002710000000075992337\\u0027,\\u0027202111050000\\u0027,\\u0027117\\u0027,30000000550913679)\\"\\u003e\\u003c/i\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\u003c/div\\u003e"}' + +it('can generate valid url', () => { + expect(url).toBe( + 'https://www.dishtv.in/WhatsonIndiaWebService.asmx/LoadPagginResultDataForProgram' + ) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result).toMatchObject({ + Channelarr: '10000000075992337', + fromdate: '202111050000', + todate: '202111060000' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: 'Thu, 04 Nov 2021 18:30:00 GMT', + stop: 'Thu, 04 Nov 2021 18:54:00 GMT', + title: 'Cynthia Williams - Diwali Look Part 01' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{"d":""}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/dsmart.com.tr/dsmart.com.tr.config.js b/sites/dsmart.com.tr/dsmart.com.tr.config.js index d545e226..20de68e1 100644 --- a/sites/dsmart.com.tr/dsmart.com.tr.config.js +++ b/sites/dsmart.com.tr/dsmart.com.tr.config.js @@ -1,104 +1,104 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules' - -module.exports = { - site: 'dsmart.com.tr', - days: 2, - url({ date, channel }) { - const [page] = channel.site_id.split('#') - - return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start - if (prev) { - start = parseStart(item, prev.stop) - } else { - start = parseStart(item, dayjs.utc(item.day)) - } - let duration = parseDuration(item) - let stop = start.add(duration, 's') - - programs.push({ - title: item.program_name, - category: parseCategory(item), - description: item.description.trim(), - start, - stop - }) - }) - - return programs - }, - async channels() { - const perPage = 1 - const totalChannels = 210 - const pages = Math.ceil(totalChannels / perPage) - - const channels = [] - for (let i in Array(pages).fill(0)) { - const page = parseInt(i) + 1 - const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format( - 'YYYY-MM-DD' - )}` - let offset = i * perPage - await axios - .get(url) - .then(r => r.data) - .then(data => { - offset++ - if (data && data.data && Array.isArray(data.data.channels)) { - data.data.channels.forEach((item, j) => { - const index = offset + j - channels.push({ - lang: 'tr', - name: item.channel_name, - site_id: index + '#' + item._id - }) - }) - } - }) - .catch(err => { - console.log(err.message) - }) - } - - return channels - } -} - -function parseCategory(item) { - return item.genre !== '0' ? item.genre : null -} - -function parseStart(item, date) { - const time = dayjs.utc(item.start_date) - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss') -} - -function parseDuration(item) { - const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/) - - return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !data.data || !Array.isArray(data.data.channels)) return null - const channelData = data.data.channels.find(i => i._id == channelId) - - return channelData && Array.isArray(channelData.schedule) ? channelData.schedule : [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.dsmart.com.tr/api/v1/public/epg/schedules' + +module.exports = { + site: 'dsmart.com.tr', + days: 2, + url({ date, channel }) { + const [page] = channel.site_id.split('#') + + return `${API_ENDPOINT}?page=${page}&limit=1&day=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start + if (prev) { + start = parseStart(item, prev.stop) + } else { + start = parseStart(item, dayjs.utc(item.day)) + } + let duration = parseDuration(item) + let stop = start.add(duration, 's') + + programs.push({ + title: item.program_name, + category: parseCategory(item), + description: item.description.trim(), + start, + stop + }) + }) + + return programs + }, + async channels() { + const perPage = 1 + const totalChannels = 210 + const pages = Math.ceil(totalChannels / perPage) + + const channels = [] + for (let i in Array(pages).fill(0)) { + const page = parseInt(i) + 1 + const url = `${API_ENDPOINT}?page=${page}&limit=${perPage}&day=${dayjs().format( + 'YYYY-MM-DD' + )}` + let offset = i * perPage + await axios + .get(url) + .then(r => r.data) + .then(data => { + offset++ + if (data && data.data && Array.isArray(data.data.channels)) { + data.data.channels.forEach((item, j) => { + const index = offset + j + channels.push({ + lang: 'tr', + name: item.channel_name, + site_id: index + '#' + item._id + }) + }) + } + }) + .catch(err => { + console.log(err.message) + }) + } + + return channels + } +} + +function parseCategory(item) { + return item.genre !== '0' ? item.genre : null +} + +function parseStart(item, date) { + const time = dayjs.utc(item.start_date) + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time.format('HH:mm:ss')}`, 'YYYY-MM-DD HH:mm:ss') +} + +function parseDuration(item) { + const [, H, mm, ss] = item.duration.match(/(\d+):(\d+):(\d+)$/) + + return parseInt(H) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !data.data || !Array.isArray(data.data.channels)) return null + const channelData = data.data.channels.find(i => i._id == channelId) + + return channelData && Array.isArray(channelData.schedule) ? channelData.schedule : [] +} diff --git a/sites/dsmart.com.tr/dsmart.com.tr.test.js b/sites/dsmart.com.tr/dsmart.com.tr.test.js index 0b2cf669..0bfdb603 100644 --- a/sites/dsmart.com.tr/dsmart.com.tr.test.js +++ b/sites/dsmart.com.tr/dsmart.com.tr.test.js @@ -1,68 +1,68 @@ -// npm run channels:parse -- --config=./sites/dsmart.com.tr/dsmart.com.tr.config.js --output=./sites/dsmart.com.tr/dsmart.com.tr.channels.xml -// npm run grab -- --site=dsmart.com.tr - -const { parser, url } = require('./dsmart.com.tr.config.js') -const dayjs = require('dayjs') -const fs = require('fs') -const path = require('path') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3#5fe07d7acfef0b1593275751', - xmltv_id: 'SinemaTV.tr' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-15T22:00:00.000Z', - stop: '2023-01-15T23:45:00.000Z', - title: 'Bizi Ayıran Her Şey', - category: 'sinema/genel', - description: - 'Issızlığın ortasında yer alan orta sınıf bir evde bir anne kız yaşamaktadır. Çevrelerindeki taşları insanlarla yaşadıkları çatışmalar, anne-kızın hayatını olumsuz yönde etkilemektedir. Kızının ansızın ortadan kaybolması, bu çatışmaların seviyesini artıracak ve anne, kızını bulmak için her türlü yola başvuracaktır.' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-15T23:45:00.000Z', - stop: '2023-01-16T01:30:00.000Z', - title: 'Pixie', - category: 'sinema/genel', - description: - 'Annesinin intikamını almak isteyen Pixie, dahiyane bir soygun planlar. Fakat işler planladığı gibi gitmeyince kendini İrlanda’nın vahşi gangsterleri tarafından kovalanan iki adamla birlikte kaçarken bulur.' - }) - - expect(results[12]).toMatchObject({ - start: '2023-01-16T20:30:00.000Z', - stop: '2023-01-16T22:30:00.000Z', - title: 'Seberg', - category: 'sinema/genel', - description: - 'Başrolünde ünlü yıldız Kristen Stewart’ın yer aldığı politik gerilim, 1960’ların sonunda insan hakları aktivisti Hakim Jamal ile yaşadığı politik ve romantik ilişki sebebiyle FBI tarafından hedef alınan, Fransız Yeni Dalgası’nın sevilen yüzü ve Serseri Aşıklar’ın yıldızı Jean Seberg’ün çarpıcı hikayesini anlatıyor.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/dsmart.com.tr/dsmart.com.tr.config.js --output=./sites/dsmart.com.tr/dsmart.com.tr.channels.xml +// npm run grab -- --site=dsmart.com.tr + +const { parser, url } = require('./dsmart.com.tr.config.js') +const dayjs = require('dayjs') +const fs = require('fs') +const path = require('path') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3#5fe07d7acfef0b1593275751', + xmltv_id: 'SinemaTV.tr' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.dsmart.com.tr/api/v1/public/epg/schedules?page=3&limit=1&day=2023-01-16' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-15T22:00:00.000Z', + stop: '2023-01-15T23:45:00.000Z', + title: 'Bizi Ayıran Her Şey', + category: 'sinema/genel', + description: + 'Issızlığın ortasında yer alan orta sınıf bir evde bir anne kız yaşamaktadır. Çevrelerindeki taşları insanlarla yaşadıkları çatışmalar, anne-kızın hayatını olumsuz yönde etkilemektedir. Kızının ansızın ortadan kaybolması, bu çatışmaların seviyesini artıracak ve anne, kızını bulmak için her türlü yola başvuracaktır.' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-15T23:45:00.000Z', + stop: '2023-01-16T01:30:00.000Z', + title: 'Pixie', + category: 'sinema/genel', + description: + 'Annesinin intikamını almak isteyen Pixie, dahiyane bir soygun planlar. Fakat işler planladığı gibi gitmeyince kendini İrlanda’nın vahşi gangsterleri tarafından kovalanan iki adamla birlikte kaçarken bulur.' + }) + + expect(results[12]).toMatchObject({ + start: '2023-01-16T20:30:00.000Z', + stop: '2023-01-16T22:30:00.000Z', + title: 'Seberg', + category: 'sinema/genel', + description: + 'Başrolünde ünlü yıldız Kristen Stewart’ın yer aldığı politik gerilim, 1960’ların sonunda insan hakları aktivisti Hakim Jamal ile yaşadığı politik ve romantik ilişki sebebiyle FBI tarafından hedef alınan, Fransız Yeni Dalgası’nın sevilen yüzü ve Serseri Aşıklar’ın yıldızı Jean Seberg’ün çarpıcı hikayesini anlatıyor.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/dstv.com/dstv.com.config.js b/sites/dstv.com/dstv.com.config.js index 8f7e62ae..02472ae3 100644 --- a/sites/dstv.com/dstv.com.config.js +++ b/sites/dstv.com/dstv.com.config.js @@ -1,101 +1,101 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' - -module.exports = { - site: 'dstv.com', - days: 2, - request: { - cache: { - ttl: 3 * 60 * 60 * 1000, // 3h - interpretHeader: false - } - }, - url: function ({ channel, date }) { - const [region] = channel.site_id.split('#') - const packageName = region === 'nga' ? '&package=DStv%20Premium' : '' - - return `${API_ENDPOINT}/GetProgrammes?d=${date.format( - 'YYYY-MM-DD' - )}${packageName}&country=${region}` - }, - async parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - for (const item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.Title, - description: parseDescription(details), - icon: parseIcon(details), - category: parseCategory(details), - start: parseTime(item.StartTime, channel), - stop: parseTime(item.EndTime, channel) - }) - } - - return programs - }, - async channels({ country }) { - const data = await axios - .get(`${API_ENDPOINT}/GetProgrammes?d=2022-03-10&package=DStv%20Premium&country=${country}`) - .then(r => r.data) - .catch(console.log) - - return data.Channels.map(item => { - return { - site_id: `${country}#${item.Number}`, - name: item.Name - } - }) - } -} - -function parseTime(time, channel) { - const [region] = channel.site_id.split('#') - const tz = { - zaf: 'Africa/Johannesburg', - nga: 'Africa/Lagos' - } - - return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region]) -} - -function parseDescription(details) { - return details ? details.Synopsis : null -} - -function parseIcon(details) { - return details ? details.ThumbnailUri : null -} - -function parseCategory(details) { - return details ? details.SubGenres : null -} - -async function loadProgramDetails(item) { - const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}` - - return axios - .get(url) - .then(r => r.data) - .catch(console.error) -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.Channels)) return [] - const channelData = data.Channels.find(c => c.Number === channelId) - if (!channelData || !Array.isArray(channelData.Programmes)) return [] - - return channelData.Programmes -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' + +module.exports = { + site: 'dstv.com', + days: 2, + request: { + cache: { + ttl: 3 * 60 * 60 * 1000, // 3h + interpretHeader: false + } + }, + url: function ({ channel, date }) { + const [region] = channel.site_id.split('#') + const packageName = region === 'nga' ? '&package=DStv%20Premium' : '' + + return `${API_ENDPOINT}/GetProgrammes?d=${date.format( + 'YYYY-MM-DD' + )}${packageName}&country=${region}` + }, + async parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + for (const item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.Title, + description: parseDescription(details), + icon: parseIcon(details), + category: parseCategory(details), + start: parseTime(item.StartTime, channel), + stop: parseTime(item.EndTime, channel) + }) + } + + return programs + }, + async channels({ country }) { + const data = await axios + .get(`${API_ENDPOINT}/GetProgrammes?d=2022-03-10&package=DStv%20Premium&country=${country}`) + .then(r => r.data) + .catch(console.log) + + return data.Channels.map(item => { + return { + site_id: `${country}#${item.Number}`, + name: item.Name + } + }) + } +} + +function parseTime(time, channel) { + const [region] = channel.site_id.split('#') + const tz = { + zaf: 'Africa/Johannesburg', + nga: 'Africa/Lagos' + } + + return dayjs.tz(time, 'YYYY-MM-DDTHH:mm:ss', tz[region]) +} + +function parseDescription(details) { + return details ? details.Synopsis : null +} + +function parseIcon(details) { + return details ? details.ThumbnailUri : null +} + +function parseCategory(details) { + return details ? details.SubGenres : null +} + +async function loadProgramDetails(item) { + const url = `${API_ENDPOINT}/GetProgramme?id=${item.Id}` + + return axios + .get(url) + .then(r => r.data) + .catch(console.error) +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.Channels)) return [] + const channelData = data.Channels.find(c => c.Number === channelId) + if (!channelData || !Array.isArray(channelData.Programmes)) return [] + + return channelData.Programmes +} diff --git a/sites/dstv.com/dstv.com.test.js b/sites/dstv.com/dstv.com.test.js index b18bb6ab..caf68f9a 100644 --- a/sites/dstv.com/dstv.com.test.js +++ b/sites/dstv.com/dstv.com.test.js @@ -1,112 +1,112 @@ -// npm run channels:parse -- --config=./sites/dstv.com/dstv.com.config.js --output=./sites/dstv.com/dstv.com.channels.xml --set=country:zaf -// npm run grab -- --site=dstv.com - -const { parser, url } = require('./dstv.com.config.js') -const axios = require('axios') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' - -const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d') -const channelZA = { - site_id: 'zaf#201', - xmltv_id: 'SuperSportGrandstand.za' -} -const channelNG = { - site_id: 'nga#201', - xmltv_id: 'SuperSportGrandstand.za' -} - -it('can generate valid url for zaf', () => { - expect(url({ channel: channelZA, date })).toBe( - `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf` - ) -}) - -it('can generate valid url for nga', () => { - expect(url({ channel: channelNG, date })).toBe( - `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga` - ) -}) - -it('can parse response for ZA', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel: channelZA }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-21T23:00:00.000Z', - stop: '2022-11-22T00:00:00.000Z', - title: 'UFC FN HL: Nzechukwu v Cutelaba', - description: - "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", - icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', - category: ['All Sport', 'Mixed Martial Arts'] - }) -}) - -it('can parse response for NG', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel: channelNG }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-21T23:00:00.000Z', - stop: '2022-11-22T00:00:00.000Z', - title: 'UFC FN HL: Nzechukwu v Cutelaba', - description: - "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", - icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', - category: ['All Sport', 'Mixed Martial Arts'] - }) -}) - -it('can handle empty guide', done => { - parser({ - content: '{"Total":0,"Channels":[]}', - channel: channelZA - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/dstv.com/dstv.com.config.js --output=./sites/dstv.com/dstv.com.channels.xml --set=country:zaf +// npm run grab -- --site=dstv.com + +const { parser, url } = require('./dstv.com.config.js') +const axios = require('axios') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const API_ENDPOINT = 'https://www.dstv.com/umbraco/api/TvGuide' + +const date = dayjs.utc('2022-11-22', 'YYYY-MM-DD').startOf('d') +const channelZA = { + site_id: 'zaf#201', + xmltv_id: 'SuperSportGrandstand.za' +} +const channelNG = { + site_id: 'nga#201', + xmltv_id: 'SuperSportGrandstand.za' +} + +it('can generate valid url for zaf', () => { + expect(url({ channel: channelZA, date })).toBe( + `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&country=zaf` + ) +}) + +it('can generate valid url for nga', () => { + expect(url({ channel: channelNG, date })).toBe( + `${API_ENDPOINT}/GetProgrammes?d=2022-11-22&package=DStv%20Premium&country=nga` + ) +}) + +it('can parse response for ZA', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zaf.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_ENDPOINT}/GetProgramme?id=8b237235-aa17-4bb8-9ea6-097e7a813336`) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_zaf.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel: channelZA }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-21T23:00:00.000Z', + stop: '2022-11-22T00:00:00.000Z', + title: 'UFC FN HL: Nzechukwu v Cutelaba', + description: + "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", + icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', + category: ['All Sport', 'Mixed Martial Arts'] + }) +}) + +it('can parse response for NG', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_nga.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_ENDPOINT}/GetProgramme?id=6d58931e-2192-486a-a202-14720136d204`) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_nga.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel: channelNG }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-21T23:00:00.000Z', + stop: '2022-11-22T00:00:00.000Z', + title: 'UFC FN HL: Nzechukwu v Cutelaba', + description: + "'UFC Fight Night Highlights - Heavyweight Bout: Kennedy Nzechukwu vs Ion Cutelaba'. From The UFC APEX Center - Las Vegas, USA.", + icon: 'https://03mcdecdnimagerepository.blob.core.windows.net/epguideimage/img/271546_UFC Fight Night.png', + category: ['All Sport', 'Mixed Martial Arts'] + }) +}) + +it('can handle empty guide', done => { + parser({ + content: '{"Total":0,"Channels":[]}', + channel: channelZA + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/elcinema.com/elcinema.com.config.js b/sites/elcinema.com/elcinema.com.config.js index 878362f9..10b03585 100644 --- a/sites/elcinema.com/elcinema.com.config.js +++ b/sites/elcinema.com/elcinema.com.config.js @@ -1,118 +1,118 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -require('dayjs/locale/ar') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'elcinema.com', - days: 2, - url({ channel }) { - const lang = channel.lang === 'en' ? 'en/' : '/' - - return `https://elcinema.com/${lang}tvguide/${channel.site_id}/` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const start = parseStart(item, date) - const duration = parseDuration(item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - category: parseCategory(item), - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - } -} - -function parseIcon(item) { - const $ = cheerio.load(item) - const imgSrc = - $('.row > div.columns.small-3.large-1 > a > img').data('src') || - $('.row > div.columns.small-5.large-1 > img').data('src') - - return imgSrc || null -} - -function parseCategory(item) { - const $ = cheerio.load(item) - const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text() - - return category.replace(/\(\d+\)/, '').trim() || null -} - -function parseDuration(item) { - const $ = cheerio.load(item) - const duration = - $('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text() - - return duration.replace(/\D/g, '') || '' -} - -function parseStart(item, initDate) { - const $ = cheerio.load(item) - let time = - $('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() || - '' - - time = time - .replace(/\[.*\]/, '') - .replace('مساءً', 'PM') - .replace('صباحًا', 'AM') - .trim() - - time = `${initDate.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess()) -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return ( - $('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() || - $('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() || - null - ) -} - -function parseDescription(item) { - const $ = cheerio.load(item) - const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || '' - - return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '') -} - -function parseItems(content, channel, date) { - const $ = cheerio.load(content) - - const dateString = date.locale(channel.lang).format('dddd D') - - const list = $('.dates') - .filter((i, el) => { - let parsedDateString = $(el).text().trim() - parsedDateString = parsedDateString.replace(/\s\s+/g, ' ') - - return parsedDateString.includes(dateString) - }) - .first() - .parent() - .next() - - return $('.padded-half', list).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +require('dayjs/locale/ar') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'elcinema.com', + days: 2, + url({ channel }) { + const lang = channel.lang === 'en' ? 'en/' : '/' + + return `https://elcinema.com/${lang}tvguide/${channel.site_id}/` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const start = parseStart(item, date) + const duration = parseDuration(item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + category: parseCategory(item), + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + } +} + +function parseIcon(item) { + const $ = cheerio.load(item) + const imgSrc = + $('.row > div.columns.small-3.large-1 > a > img').data('src') || + $('.row > div.columns.small-5.large-1 > img').data('src') + + return imgSrc || null +} + +function parseCategory(item) { + const $ = cheerio.load(item) + const category = $('.row > div.columns.small-6.large-3 > ul > li:nth-child(2)').text() + + return category.replace(/\(\d+\)/, '').trim() || null +} + +function parseDuration(item) { + const $ = cheerio.load(item) + const duration = + $('.row > div.columns.small-3.large-2 > ul > li:nth-child(2) > span').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2) > span').text() + + return duration.replace(/\D/g, '') || '' +} + +function parseStart(item, initDate) { + const $ = cheerio.load(item) + let time = + $('.row > div.columns.small-3.large-2 > ul > li:nth-child(1)').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(2)').text() || + '' + + time = time + .replace(/\[.*\]/, '') + .replace('مساءً', 'PM') + .replace('صباحًا', 'AM') + .trim() + + time = `${initDate.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD hh:mm A', dayjs.tz.guess()) +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return ( + $('.row > div.columns.small-6.large-3 > ul > li:nth-child(1) > a').text() || + $('.row > div.columns.small-7.large-11 > ul > li:nth-child(1)').text() || + null + ) +} + +function parseDescription(item) { + const $ = cheerio.load(item) + const excerpt = $('.row > div.columns.small-12.large-6 > ul > li:nth-child(3)').text() || '' + + return excerpt.replace('...اقرأ المزيد', '').replace('...Read more', '') +} + +function parseItems(content, channel, date) { + const $ = cheerio.load(content) + + const dateString = date.locale(channel.lang).format('dddd D') + + const list = $('.dates') + .filter((i, el) => { + let parsedDateString = $(el).text().trim() + parsedDateString = parsedDateString.replace(/\s\s+/g, ' ') + + return parsedDateString.includes(dateString) + }) + .first() + .parent() + .next() + + return $('.padded-half', list).toArray() +} diff --git a/sites/elcinema.com/elcinema.com.test.js b/sites/elcinema.com/elcinema.com.test.js index d7ea6e93..3e8e14f7 100644 --- a/sites/elcinema.com/elcinema.com.test.js +++ b/sites/elcinema.com/elcinema.com.test.js @@ -1,69 +1,69 @@ -// npm run grab -- --site=elcinema.com - -const { parser, url } = require('./elcinema.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channelAR = { - lang: 'ar', - site_id: '1254', - xmltv_id: 'OSNSeries.ae' -} -const channelEN = { - lang: 'en', - site_id: '1254', - xmltv_id: 'OSNSeries.ae' -} - -it('can generate valid url', () => { - expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/') -}) - -it('can parse response (en)', () => { - const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html')) - const results = parser({ date, channel: channelEN, content: contentEN }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-27T14:25:00.000Z', - stop: '2022-08-27T15:15:00.000Z', - title: 'Station 19 S5', - icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', - category: 'Series' - }) -}) - -it('can parse response (ar)', () => { - const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html')) - const results = parser({ date, channel: channelAR, content: contentAR }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-27T14:25:00.000Z', - stop: '2022-08-27T15:15:00.000Z', - title: 'Station 19 S5', - icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', - category: 'مسلسل' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel: channelEN, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=elcinema.com + +const { parser, url } = require('./elcinema.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channelAR = { + lang: 'ar', + site_id: '1254', + xmltv_id: 'OSNSeries.ae' +} +const channelEN = { + lang: 'en', + site_id: '1254', + xmltv_id: 'OSNSeries.ae' +} + +it('can generate valid url', () => { + expect(url({ channel: channelEN })).toBe('https://elcinema.com/en/tvguide/1254/') +}) + +it('can parse response (en)', () => { + const contentEN = fs.readFileSync(path.resolve(__dirname, '__data__/content.en.html')) + const results = parser({ date, channel: channelEN, content: contentEN }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-27T14:25:00.000Z', + stop: '2022-08-27T15:15:00.000Z', + title: 'Station 19 S5', + icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', + category: 'Series' + }) +}) + +it('can parse response (ar)', () => { + const contentAR = fs.readFileSync(path.resolve(__dirname, '__data__/content.ar.html')) + const results = parser({ date, channel: channelAR, content: contentAR }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-27T14:25:00.000Z', + stop: '2022-08-27T15:15:00.000Z', + title: 'Station 19 S5', + icon: 'https://media.elcinema.com/uploads/_150x200_ec30d1a2251c8edf83334be4860184c74d2534d7ba508a334ad66fa59acc4926.jpg', + category: 'مسلسل' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel: channelEN, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js index d8401689..e94f170a 100644 --- a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js +++ b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.config.js @@ -1,68 +1,68 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'ena.skylifetv.co.kr', - days: 2, - url({ channel, date }) { - return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.col2 > .tit').text().trim() -} - -function parseRating($item) { - const rating = $item('.col4').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseDuration($item) { - const duration = $item('.col5').text().trim() - - return duration ? parseInt(duration) : 30 -} - -function parseStart($item, date) { - const time = $item('.col1').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.tbl_schedule > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ena.skylifetv.co.kr', + days: 2, + url({ channel, date }) { + return `http://ena.skylifetv.co.kr/${channel.site_id}/?day=${date.format('YYYYMMDD')}&sc_dvsn=U` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.col2 > .tit').text().trim() +} + +function parseRating($item) { + const rating = $item('.col4').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseDuration($item) { + const duration = $item('.col5').text().trim() + + return duration ? parseInt(duration) : 30 +} + +function parseStart($item, date) { + const time = $item('.col1').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.tbl_schedule > tbody > tr').toArray() +} diff --git a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js index 12c9255c..78728b68 100644 --- a/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js +++ b/sites/ena.skylifetv.co.kr/ena.skylifetv.co.kr.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=ena.skylifetv.co.kr - -const { parser, url } = require('./ena.skylifetv.co.kr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ENA', - xmltv_id: 'ENA.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-26T16:05:00.000Z', - stop: '2023-01-26T17:20:00.000Z', - title: '법쩐 6화', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[17]).toMatchObject({ - start: '2023-01-27T14:10:00.000Z', - stop: '2023-01-27T15:25:00.000Z', - title: '남이 될 수 있을까 4화', - rating: { - system: 'KMRB', - value: '15' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=ena.skylifetv.co.kr + +const { parser, url } = require('./ena.skylifetv.co.kr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ENA', + xmltv_id: 'ENA.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('http://ena.skylifetv.co.kr/ENA/?day=20230127&sc_dvsn=U') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-26T16:05:00.000Z', + stop: '2023-01-26T17:20:00.000Z', + title: '법쩐 6화', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[17]).toMatchObject({ + start: '2023-01-27T14:10:00.000Z', + stop: '2023-01-27T15:25:00.000Z', + title: '남이 될 수 있을까 4화', + rating: { + system: 'KMRB', + value: '15' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/entertainment.ie/entertainment.ie.config.js b/sites/entertainment.ie/entertainment.ie.config.js index 0ac331d1..65df3b86 100644 --- a/sites/entertainment.ie/entertainment.ie.config.js +++ b/sites/entertainment.ie/entertainment.ie.config.js @@ -1,95 +1,95 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'entertainment.ie', - days: 2, - url: function ({ date, channel }) { - return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( - 'DD-MM-YYYY' - )}&time=all-day` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - } - const duration = parseDuration($item) - const stop = start.plus({ minutes: duration }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - categories: parseCategories($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://entertainment.ie/tv/all-channels/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - let channels = $('.tv-filter-container > tv-filter').attr(':channels') - channels = JSON.parse(channels) - - return channels.map(c => { - return { - site_id: c.slug, - name: c.name - } - }) - } -} - -function parseIcon($item) { - return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') -} - -function parseTitle($item) { - return $item('.text-holder h3').text().trim() -} - -function parseDescription($item) { - return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') -} - -function parseCategories($item) { - const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') - - return genres ? genres.split(', ') : [] -} - -function parseStart($item, date) { - let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') - let [, time] = d ? d.split(', ') : [null, null] - - return time - ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'UTC' - }).toUTC() - : null -} - -function parseDuration($item) { - const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') - - return parseInt(duration) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.info-list > li').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'entertainment.ie', + days: 2, + url: function ({ date, channel }) { + return `https://entertainment.ie/tv/${channel.site_id}/?date=${date.format( + 'DD-MM-YYYY' + )}&time=all-day` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + } + const duration = parseDuration($item) + const stop = start.plus({ minutes: duration }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + categories: parseCategories($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://entertainment.ie/tv/all-channels/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + let channels = $('.tv-filter-container > tv-filter').attr(':channels') + channels = JSON.parse(channels) + + return channels.map(c => { + return { + site_id: c.slug, + name: c.name + } + }) + } +} + +function parseIcon($item) { + return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('img') +} + +function parseTitle($item) { + return $item('.text-holder h3').text().trim() +} + +function parseDescription($item) { + return $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('description') +} + +function parseCategories($item) { + const genres = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('genres') + + return genres ? genres.split(', ') : [] +} + +function parseStart($item, date) { + let d = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('time') + let [, time] = d ? d.split(', ') : [null, null] + + return time + ? DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'UTC' + }).toUTC() + : null +} + +function parseDuration($item) { + const duration = $item('.text-holder > .btn-hold > .btn-wrap > a.btn-share').data('duration') + + return parseInt(duration) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.info-list > li').toArray() +} diff --git a/sites/entertainment.ie/entertainment.ie.test.js b/sites/entertainment.ie/entertainment.ie.test.js index 4f027098..beb41cdf 100644 --- a/sites/entertainment.ie/entertainment.ie.test.js +++ b/sites/entertainment.ie/entertainment.ie.test.js @@ -1,59 +1,59 @@ -// npm run channels:parse -- --config=./sites/entertainment.ie/entertainment.ie.config.js --output=./sites/entertainment.ie/entertainment.ie.channels.xml -// npm run grab -- --site=entertainment.ie - -const fs = require('fs') -const path = require('path') -const { parser, url } = require('./entertainment.ie.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(51) - - expect(results[0]).toMatchObject({ - start: '2023-06-29T06:00:00.000Z', - stop: '2023-06-29T08:00:00.000Z', - title: 'EuroNews', - description: 'European and international headlines live via satellite', - icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', - categories: ['Factual'] - }) - - expect(results[50]).toMatchObject({ - start: '2023-06-30T02:25:00.000Z', - stop: '2023-06-30T06:00:00.000Z', - title: 'EuroNews', - description: 'European and international headlines live via satellite', - icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', - categories: ['Factual'] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) - const result = parser({ - date, - channel, - content - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/entertainment.ie/entertainment.ie.config.js --output=./sites/entertainment.ie/entertainment.ie.channels.xml +// npm run grab -- --site=entertainment.ie + +const fs = require('fs') +const path = require('path') +const { parser, url } = require('./entertainment.ie.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-29', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'rte2', xmltv_id: 'RTE2.ie' } + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://entertainment.ie/tv/rte2/?date=29-06-2023&time=all-day' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(51) + + expect(results[0]).toMatchObject({ + start: '2023-06-29T06:00:00.000Z', + stop: '2023-06-29T08:00:00.000Z', + title: 'EuroNews', + description: 'European and international headlines live via satellite', + icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', + categories: ['Factual'] + }) + + expect(results[50]).toMatchObject({ + start: '2023-06-30T02:25:00.000Z', + stop: '2023-06-30T06:00:00.000Z', + title: 'EuroNews', + description: 'European and international headlines live via satellite', + icon: 'https://img.resized.co/entertainment/eyJkYXRhIjoie1widXJsXCI6XCJodHRwczpcXFwvXFxcL3R2LmFzc2V0cy5wcmVzc2Fzc29jaWF0aW9uLmlvXFxcLzcxZDdkYWY2LWQxMjItNTliYy1iMGRjLTFkMjc2ODg1MzhkNC5qcGdcIixcIndpZHRoXCI6NDgwLFwiaGVpZ2h0XCI6Mjg4LFwiZGVmYXVsdFwiOlwiaHR0cHM6XFxcL1xcXC9lbnRlcnRhaW5tZW50LmllXFxcL2ltYWdlc1xcXC9uby1pbWFnZS5wbmdcIn0iLCJoYXNoIjoiZDhjYzA0NzFhMGZhOTI1Yjc5ODI0M2E3OWZjMGI2ZGJmMDIxMjllNyJ9/71d7daf6-d122-59bc-b0dc-1d27688538d4.jpg', + categories: ['Factual'] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) + const result = parser({ + date, + channel, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/epg.i-cable.com/epg.i-cable.com.config.js b/sites/epg.i-cable.com/epg.i-cable.com.config.js index 20074174..4a41713c 100644 --- a/sites/epg.i-cable.com/epg.i-cable.com.config.js +++ b/sites/epg.i-cable.com/epg.i-cable.com.config.js @@ -1,89 +1,89 @@ -const axios = require('axios') -const { DateTime } = require('luxon') - -const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel' - -module.exports = { - site: 'epg.i-cable.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - const stop = start.plus({ minutes: 30 }) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - programs.push({ - title: parseTitle(item, channel), - start, - stop - }) - } - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`${API_ENDPOINT}/category/0?api=api`) - .then(r => r.data) - .catch(console.error) - - let channels = [] - const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`)) - await Promise.allSettled(promises).then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - channels = channels.concat(r.value.data.chs) - } - }) - }) - - return channels.map(c => { - let name = lang === 'zh' ? c.channel_name : c.channel_name_en - name = c.remark_id == 3 ? `${name} [HD]` : name - - return { - site_id: c.channel_no, - name, - lang - } - }) - } -} - -function parseTitle(item, channel) { - return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi -} - -function parseStart(item, date) { - let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM' - return DateTime.fromFormat( - `${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`, - 'yyyy-MM-dd hh:mm a', - { - zone: 'Asia/Hong_Kong' - } - ).toUTC() -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.epgs)) return [] - - return data.epgs -} +const axios = require('axios') +const { DateTime } = require('luxon') + +const API_ENDPOINT = 'http://epg.i-cable.com/ci/channel' + +module.exports = { + site: 'epg.i-cable.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}?api=api` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + const stop = start.plus({ minutes: 30 }) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + programs.push({ + title: parseTitle(item, channel), + start, + stop + }) + } + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`${API_ENDPOINT}/category/0?api=api`) + .then(r => r.data) + .catch(console.error) + + let channels = [] + const promises = data.cates.map(c => axios.get(`${API_ENDPOINT}/category/${c.cate_id}?api=api`)) + await Promise.allSettled(promises).then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + channels = channels.concat(r.value.data.chs) + } + }) + }) + + return channels.map(c => { + let name = lang === 'zh' ? c.channel_name : c.channel_name_en + name = c.remark_id == 3 ? `${name} [HD]` : name + + return { + site_id: c.channel_no, + name, + lang + } + }) + } +} + +function parseTitle(item, channel) { + return channel.lang === 'en' ? item.programme_name_eng : item.programme_name_chi +} + +function parseStart(item, date) { + let meridiem = item.session_mark === 'PM' ? 'PM' : 'AM' + return DateTime.fromFormat( + `${date.format('YYYY-MM-DD')} ${item.time} ${meridiem}`, + 'yyyy-MM-dd hh:mm a', + { + zone: 'Asia/Hong_Kong' + } + ).toUTC() +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.epgs)) return [] + + return data.epgs +} diff --git a/sites/epg.i-cable.com/epg.i-cable.com.test.js b/sites/epg.i-cable.com/epg.i-cable.com.test.js index 8fc59b98..1982ce16 100644 --- a/sites/epg.i-cable.com/epg.i-cable.com.test.js +++ b/sites/epg.i-cable.com/epg.i-cable.com.test.js @@ -1,73 +1,73 @@ -// npm run channels:parse -- --config=./sites/epg.i-cable.com/epg.i-cable.com.config.js --output=./sites/epg.i-cable.com/epg.i-cable.com.channels.xml --set=lang:zh -// npm run grab -- --site=epg.i-cable.com - -const { parser, url } = require('./epg.i-cable.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '003', - xmltv_id: 'HOYTV.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: 'Bloomberg 時段' - }) - - expect(results[31]).toMatchObject({ - start: '2022-11-15T21:00:00.000Z', - stop: '2022-11-15T21:30:00.000Z', - title: 'Bloomberg 時段' - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const channelEN = { ...channel, lang: 'en' } - let results = parser({ content, channel: channelEN, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: 'Bloomberg Hour' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/epg.i-cable.com/epg.i-cable.com.config.js --output=./sites/epg.i-cable.com/epg.i-cable.com.channels.xml --set=lang:zh +// npm run grab -- --site=epg.i-cable.com + +const { parser, url } = require('./epg.i-cable.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '003', + xmltv_id: 'HOYTV.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://epg.i-cable.com/ci/channel/epg/003/2022-11-15?api=api' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: 'Bloomberg 時段' + }) + + expect(results[31]).toMatchObject({ + start: '2022-11-15T21:00:00.000Z', + stop: '2022-11-15T21:30:00.000Z', + title: 'Bloomberg 時段' + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const channelEN = { ...channel, lang: 'en' } + let results = parser({ content, channel: channelEN, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: 'Bloomberg Hour' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/firstmedia.com/firstmedia.com.config.js b/sites/firstmedia.com/firstmedia.com.config.js index 75237b1e..ec62c1a6 100644 --- a/sites/firstmedia.com/firstmedia.com.config.js +++ b/sites/firstmedia.com/firstmedia.com.config.js @@ -1,52 +1,52 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(timezone) -dayjs.extend(utc) - -module.exports = { - site: 'firstmedia.com', - days: 1, - url: function ({ channel, date }) { - return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${ - channel.site_id - }&start_time=1&end_time=24&need_channels=0` - }, - parser: function ({ content, channel }) { - if (!content || !channel) return [] - - let programs = [] - const items = parseItems(content, channel.site_id) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - start: parseStart(item).toISOString(), - stop: parseStop(item).toISOString() - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - return JSON.parse(content.trim()).entries[channel] -} - -function parseTitle(item) { - return item.title -} - -function parseDescription(item) { - return item.long_description -} - -function parseStart(item) { - return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') -} - -function parseStop(item) { - return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(timezone) +dayjs.extend(utc) + +module.exports = { + site: 'firstmedia.com', + days: 1, + url: function ({ channel, date }) { + return `https://www.firstmedia.com/ajax/schedule?date=${date.format('DD/MM/YYYY')}&channel=${ + channel.site_id + }&start_time=1&end_time=24&need_channels=0` + }, + parser: function ({ content, channel }) { + if (!content || !channel) return [] + + let programs = [] + const items = parseItems(content, channel.site_id) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + start: parseStart(item).toISOString(), + stop: parseStop(item).toISOString() + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + return JSON.parse(content.trim()).entries[channel] +} + +function parseTitle(item) { + return item.title +} + +function parseDescription(item) { + return item.long_description +} + +function parseStart(item) { + return dayjs.tz(item.start_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') +} + +function parseStop(item) { + return dayjs.tz(item.end_time, 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') +} diff --git a/sites/firstmedia.com/firstmedia.com.test.js b/sites/firstmedia.com/firstmedia.com.test.js index 821fdaac..9e717c74 100644 --- a/sites/firstmedia.com/firstmedia.com.test.js +++ b/sites/firstmedia.com/firstmedia.com.test.js @@ -1,35 +1,35 @@ -const { url, parser } = require('./firstmedia.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d') -const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.firstmedia.com/ajax/schedule?date=18/06/2023&channel=251&start_time=1&end_time=24&need_channels=0' - ) -}) - -it('can parse response', () => { - const content = - '{"entries":{"251":[{"logo":"files/images/d/new-logo/channels/11-NEWS/ABC Australia SD-FirstMedia-Chl-251.jpg","name":"ABC Australia","id":"2a800e8a-fdcc-47b3-a4a6-58d1d122b326","channel_id":"a1840c59-6c92-8233-3a02-230246aae0c4","channel_no":251,"programme_id":null,"episode":null,"title":"China Tonight","slug":null,"date":"2023-06-13 00:00:00","start_time":"2023-06-13 10:55:00","end_time":"2023-06-13 11:30:00","length":2100,"description":"China Tonight","long_description":"China is a superpower that dominates global news but it\'s also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today.","status":"0","created_by":null,"updated_by":null,"created_at":"2023-06-13 00:20:24","updated_at":"2023-06-13 00:20:24"}]}}' - const results = parser({ content, channel }) - - expect(results).toMatchObject([ - { - start: '2023-06-13T03:55:00.000Z', - stop: '2023-06-13T04:30:00.000Z', - title: 'China Tonight', - description: - "China is a superpower that dominates global news but it's also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today." - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +const { url, parser } = require('./firstmedia.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-18', 'DD/MM/YYYY').startOf('d') +const channel = { site_id: '251', xmltv_id: 'ABCAustralia.au', lang: 'id' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.firstmedia.com/ajax/schedule?date=18/06/2023&channel=251&start_time=1&end_time=24&need_channels=0' + ) +}) + +it('can parse response', () => { + const content = + '{"entries":{"251":[{"logo":"files/images/d/new-logo/channels/11-NEWS/ABC Australia SD-FirstMedia-Chl-251.jpg","name":"ABC Australia","id":"2a800e8a-fdcc-47b3-a4a6-58d1d122b326","channel_id":"a1840c59-6c92-8233-3a02-230246aae0c4","channel_no":251,"programme_id":null,"episode":null,"title":"China Tonight","slug":null,"date":"2023-06-13 00:00:00","start_time":"2023-06-13 10:55:00","end_time":"2023-06-13 11:30:00","length":2100,"description":"China Tonight","long_description":"China is a superpower that dominates global news but it\'s also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today.","status":"0","created_by":null,"updated_by":null,"created_at":"2023-06-13 00:20:24","updated_at":"2023-06-13 00:20:24"}]}}' + const results = parser({ content, channel }) + + expect(results).toMatchObject([ + { + start: '2023-06-13T03:55:00.000Z', + stop: '2023-06-13T04:30:00.000Z', + title: 'China Tonight', + description: + "China is a superpower that dominates global news but it's also home to 1.4 billion stories. Sam Yang is back for a new season, hearing from the people who make this extraordinary nation what it is today." + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/flixed.io/flixed.io.config.js b/sites/flixed.io/flixed.io.config.js index 5dfb5e0a..31513097 100644 --- a/sites/flixed.io/flixed.io.config.js +++ b/sites/flixed.io/flixed.io.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'flixed.io', - days: 1, // NOTE: changing the date in a request does not change the response - url: function ({ date, channel }) { - return `https://tv-guide.vercel.app/api/stationAirings?stationId=${ - channel.site_id - }&startDateTime=${date.toJSON()}` - }, - parser({ content }) { - let programs = [] - let items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.program.title, - description: item.program.longDescription, - category: item.program.subType, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseIcon(item) { - const uri = item.program.preferredImage.uri - - return uri ? `https://adma.tmsimg.com/assets/${uri}` : null -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content) { - return JSON.parse(content) -} +const dayjs = require('dayjs') + +module.exports = { + site: 'flixed.io', + days: 1, // NOTE: changing the date in a request does not change the response + url: function ({ date, channel }) { + return `https://tv-guide.vercel.app/api/stationAirings?stationId=${ + channel.site_id + }&startDateTime=${date.toJSON()}` + }, + parser({ content }) { + let programs = [] + let items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.program.title, + description: item.program.longDescription, + category: item.program.subType, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseIcon(item) { + const uri = item.program.preferredImage.uri + + return uri ? `https://adma.tmsimg.com/assets/${uri}` : null +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content) { + return JSON.parse(content) +} diff --git a/sites/flixed.io/flixed.io.test.js b/sites/flixed.io/flixed.io.test.js index e22d4b15..14888ec3 100644 --- a/sites/flixed.io/flixed.io.test.js +++ b/sites/flixed.io/flixed.io.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=flixed.io - -const { parser, url } = require('./flixed.io.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '108970', - xmltv_id: 'VSiN.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://tv-guide.vercel.app/api/stationAirings?stationId=108970&startDateTime=2023-01-19T00:00:00.000Z' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-19T05:00:00.000Z', - stop: '2023-01-19T06:00:00.000Z', - title: 'The Greg Peterson Experience', - category: 'Sports non-event', - icon: 'https://adma.tmsimg.com/assets/assets/p20628892_b_v13_aa.jpg?w=270&h=360', - description: 'A different kind of sports betting.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '[]' - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=flixed.io + +const { parser, url } = require('./flixed.io.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '108970', + xmltv_id: 'VSiN.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://tv-guide.vercel.app/api/stationAirings?stationId=108970&startDateTime=2023-01-19T00:00:00.000Z' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-19T05:00:00.000Z', + stop: '2023-01-19T06:00:00.000Z', + title: 'The Greg Peterson Experience', + category: 'Sports non-event', + icon: 'https://adma.tmsimg.com/assets/assets/p20628892_b_v13_aa.jpg?w=270&h=360', + description: 'A different kind of sports betting.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '[]' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/foxsports.com.au/foxsports.com.au.config.js b/sites/foxsports.com.au/foxsports.com.au.config.js index ec4d346b..4eba7c47 100644 --- a/sites/foxsports.com.au/foxsports.com.au.config.js +++ b/sites/foxsports.com.au/foxsports.com.au.config.js @@ -1,42 +1,42 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'foxsports.com.au', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format( - 'YYYY-MM-DD' - )}&to=${date.add(1, 'd').format('YYYY-MM-DD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.programmeTitle, - sub_title: item.title, - category: item.genreTitle, - description: item.synopsis, - start: dayjs.utc(item.startTime), - stop: dayjs.utc(item.endTime) - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data) return [] - const programmes = data['channel-programme'] - if (!Array.isArray(programmes)) return [] - - const channelData = programmes.filter(i => i.channelId == channel.site_id) - return channelData && Array.isArray(channelData) ? channelData : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'foxsports.com.au', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://tvguide.foxsports.com.au/granite-api/programmes.json?from=${date.format( + 'YYYY-MM-DD' + )}&to=${date.add(1, 'd').format('YYYY-MM-DD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.programmeTitle, + sub_title: item.title, + category: item.genreTitle, + description: item.synopsis, + start: dayjs.utc(item.startTime), + stop: dayjs.utc(item.endTime) + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data) return [] + const programmes = data['channel-programme'] + if (!Array.isArray(programmes)) return [] + + const channelData = programmes.filter(i => i.channelId == channel.site_id) + return channelData && Array.isArray(channelData) ? channelData : [] +} diff --git a/sites/foxsports.com.au/foxsports.com.au.test.js b/sites/foxsports.com.au/foxsports.com.au.test.js index 52090f73..8025d55b 100644 --- a/sites/foxsports.com.au/foxsports.com.au.test.js +++ b/sites/foxsports.com.au/foxsports.com.au.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=foxsports.com.au - -const { parser, url } = require('./foxsports.com.au.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'FoxLeague.au' -} -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15' - ) -}) - -it('can parse response', () => { - const content = - '{"channel-programme":[{"id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07","programmeTitle":"NRL","title":"Eels v Titans","startTime":"2022-12-14T00:00:00+11:00","endTime":"2022-12-14T01:00:00+11:00","duration":60,"live":false,"genreId":"5c389cf4-8db7-4b52-9773-52355bd28559","channelId":2,"channelName":"FOX League","channelAbbreviation":"LEAGUE","programmeUID":235220,"round":"R1","statsMatchId":null,"closedCaptioned":true,"statsFixtureId":10207,"genreTitle":"Rugby League","parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11","parentGenreTitle":"Sport","pmgId":"PMG01306944","statsSport":"league","type":"GAME","hiDef":true,"widescreen":true,"classification":"","synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.","preGameStartTime":null,"closeCaptioned":true}]}' - - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'NRL', - sub_title: 'Eels v Titans', - description: - 'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.', - category: 'Rugby League', - start: '2022-12-13T13:00:00.000Z', - stop: '2022-12-13T14:00:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: '{"channel-programme":[]}' - }, - channel - ) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=foxsports.com.au + +const { parser, url } = require('./foxsports.com.au.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'FoxLeague.au' +} +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://tvguide.foxsports.com.au/granite-api/programmes.json?from=2022-12-14&to=2022-12-15' + ) +}) + +it('can parse response', () => { + const content = + '{"channel-programme":[{"id":"31cc8b4c-3711-49f0-bf22-2ec3993b0a07","programmeTitle":"NRL","title":"Eels v Titans","startTime":"2022-12-14T00:00:00+11:00","endTime":"2022-12-14T01:00:00+11:00","duration":60,"live":false,"genreId":"5c389cf4-8db7-4b52-9773-52355bd28559","channelId":2,"channelName":"FOX League","channelAbbreviation":"LEAGUE","programmeUID":235220,"round":"R1","statsMatchId":null,"closedCaptioned":true,"statsFixtureId":10207,"genreTitle":"Rugby League","parentGenreId":"a953f929-2d12-41a4-b0e9-97f401afff11","parentGenreTitle":"Sport","pmgId":"PMG01306944","statsSport":"league","type":"GAME","hiDef":true,"widescreen":true,"classification":"","synopsis":"The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.","preGameStartTime":null,"closeCaptioned":true}]}' + + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'NRL', + sub_title: 'Eels v Titans', + description: + 'The Eels and Titans have plenty of motivation this season after heartbreaking Finals losses in 2021. Parramatta has won their past five against Gold Coast.', + category: 'Rugby League', + start: '2022-12-13T13:00:00.000Z', + stop: '2022-12-13T14:00:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser( + { + content: '{"channel-programme":[]}' + }, + channel + ) + expect(result).toMatchObject([]) +}) diff --git a/sites/foxtel.com.au/foxtel.com.au.config.js b/sites/foxtel.com.au/foxtel.com.au.config.js index e8674256..97ee3590 100644 --- a/sites/foxtel.com.au/foxtel.com.au.config.js +++ b/sites/foxtel.com.au/foxtel.com.au.config.js @@ -1,134 +1,134 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') - -module.exports = { - site: 'foxtel.com.au', - days: 2, - url({ channel, date }) { - return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format( - 'YYYY/MM/DD' - )}` - }, - request: { - headers: { - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|6' - } - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - icon: parseIcon($item), - rating: parseRating($item), - season: parseSeason($item), - episode: parseEpisode($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', { - headers: { - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|6' - } - }) - .then(r => r.data) - .catch(console.log) - - return data.channels.forEach(item => { - let name = item.name.replace(/\+/g, '-').replace(/&/g, '') - const slug = name.replace(/[^a-z0-9\s]/gi, '').replace(/[^a-z0-9]/i, '-') - - return { - name: item.name.replace(/&/g, '&'), - site_id: `${slug}/${item.channelTag}` - } - }) - } -} - -function parseSeason($item) { - let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title') - if (!seasonString) return null - let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title') - if (!episodeString) return null - let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseIcon($item) { - return $item('.epg-event-thumbnail > img').attr('src') -} - -function parseTitle($item) { - return $item('.epg-event-description').clone().children().remove().end().text().trim() -} - -function parseSubTitle($item) { - let subtitle = $item('.epg-event-description > div') - .clone() - .children() - .remove() - .end() - .text() - .trim() - .split(',') - - subtitle = subtitle.pop() - const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] - - return subtitle.replace(`(${rating})`, '').trim() -} - -function parseRating($item) { - const subtitle = $item('.epg-event-description > div').text().trim() - const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] - - return rating - ? { - system: 'ACB', - value: rating - } - : null -} - -function parseStart($item) { - const unix = $item('*').data('scheduled-date') - - return dayjs(parseInt(unix)) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('#epg-channel-events > a').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') + +module.exports = { + site: 'foxtel.com.au', + days: 2, + url({ channel, date }) { + return `https://www.foxtel.com.au/tv-guide/channel/${channel.site_id}/${date.format( + 'YYYY/MM/DD' + )}` + }, + request: { + headers: { + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|6' + } + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + icon: parseIcon($item), + rating: parseRating($item), + season: parseSeason($item), + episode: parseEpisode($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://www.foxtel.com.au/webepg/ws/foxtel/channels?regionId=8336', { + headers: { + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|6' + } + }) + .then(r => r.data) + .catch(console.log) + + return data.channels.forEach(item => { + let name = item.name.replace(/\+/g, '-').replace(/&/g, '') + const slug = name.replace(/[^a-z0-9\s]/gi, '').replace(/[^a-z0-9]/i, '-') + + return { + name: item.name.replace(/&/g, '&'), + site_id: `${slug}/${item.channelTag}` + } + }) + } +} + +function parseSeason($item) { + let seasonString = $item('.epg-event-description > div > abbr:nth-child(1)').attr('title') + if (!seasonString) return null + let [, season] = seasonString.match(/^Season: (\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + let episodeString = $item('.epg-event-description > div > abbr:nth-child(2)').attr('title') + if (!episodeString) return null + let [, episode] = episodeString.match(/^Episode: (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseIcon($item) { + return $item('.epg-event-thumbnail > img').attr('src') +} + +function parseTitle($item) { + return $item('.epg-event-description').clone().children().remove().end().text().trim() +} + +function parseSubTitle($item) { + let subtitle = $item('.epg-event-description > div') + .clone() + .children() + .remove() + .end() + .text() + .trim() + .split(',') + + subtitle = subtitle.pop() + const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] + + return subtitle.replace(`(${rating})`, '').trim() +} + +function parseRating($item) { + const subtitle = $item('.epg-event-description > div').text().trim() + const [, rating] = subtitle.match(/\(([^)]+)\)$/) || [null, null] + + return rating + ? { + system: 'ACB', + value: rating + } + : null +} + +function parseStart($item) { + const unix = $item('*').data('scheduled-date') + + return dayjs(parseInt(unix)) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('#epg-channel-events > a').toArray() +} diff --git a/sites/foxtel.com.au/foxtel.com.au.test.js b/sites/foxtel.com.au/foxtel.com.au.test.js index a3779b7f..35394654 100644 --- a/sites/foxtel.com.au/foxtel.com.au.test.js +++ b/sites/foxtel.com.au/foxtel.com.au.test.js @@ -1,62 +1,62 @@ -// npm run channels:parse -- --config=./sites/foxtel.com.au/foxtel.com.au.config.js --output=./sites/foxtel.com.au/foxtel.com.au.channels.xml -// npm run grab -- --site=foxtel.com.au - -const { parser, url, request } = require('./foxtel.com.au.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Channel-9-Sydney/NIN', - xmltv_id: 'Channel9Sydney.au' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Accept-Language': 'en-US,en;', - Cookie: 'AAMC_foxtel_0=REGION|6' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-07T12:40:00.000Z', - stop: '2022-11-07T13:30:00.000Z', - title: 'The Equalizer', - sub_title: 'Glory', - icon: 'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830', - rating: { - system: 'ACB', - value: 'M' - }, - season: 1, - episode: 2 - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/foxtel.com.au/foxtel.com.au.config.js --output=./sites/foxtel.com.au/foxtel.com.au.channels.xml +// npm run grab -- --site=foxtel.com.au + +const { parser, url, request } = require('./foxtel.com.au.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Channel-9-Sydney/NIN', + xmltv_id: 'Channel9Sydney.au' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.foxtel.com.au/tv-guide/channel/Channel-9-Sydney/NIN/2022/11/08' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Accept-Language': 'en-US,en;', + Cookie: 'AAMC_foxtel_0=REGION|6' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-07T12:40:00.000Z', + stop: '2022-11-07T13:30:00.000Z', + title: 'The Equalizer', + sub_title: 'Glory', + icon: 'https://images1.resources.foxtel.com.au/store2/mount1/16/3/69e0v.jpg?maxheight=90&limit=91aa1c7a2c485aeeba0706941f79f111adb35830', + rating: { + system: 'ACB', + value: 'M' + }, + season: 1, + episode: 2 + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/frikanalen.no/frikanalen.no.config.js b/sites/frikanalen.no/frikanalen.no.config.js index e66a6efe..a5886752 100644 --- a/sites/frikanalen.no/frikanalen.no.config.js +++ b/sites/frikanalen.no/frikanalen.no.config.js @@ -1,52 +1,52 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'frikanalen.no', - days: 2, - url({ date }) { - return `https://frikanalen.no/api/scheduleitems/?date=${date.format( - 'YYYY-MM-DD' - )}&format=json&limit=100` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - category: parseCategory(item), - description: parseDescription(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseTitle(item) { - return item.video.name -} - -function parseCategory(item) { - return item.video.categories -} - -function parseDescription(item) { - return item.video.header -} - -function parseStart(item) { - return dayjs(item.starttime) -} - -function parseStop(item) { - return dayjs(item.endtime) -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data && Array.isArray(data.results) ? data.results : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'frikanalen.no', + days: 2, + url({ date }) { + return `https://frikanalen.no/api/scheduleitems/?date=${date.format( + 'YYYY-MM-DD' + )}&format=json&limit=100` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + category: parseCategory(item), + description: parseDescription(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseTitle(item) { + return item.video.name +} + +function parseCategory(item) { + return item.video.categories +} + +function parseDescription(item) { + return item.video.header +} + +function parseStart(item) { + return dayjs(item.starttime) +} + +function parseStop(item) { + return dayjs(item.endtime) +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data && Array.isArray(data.results) ? data.results : [] +} diff --git a/sites/frikanalen.no/frikanalen.no.test.js b/sites/frikanalen.no/frikanalen.no.test.js index 8c9fb2da..37a24903 100644 --- a/sites/frikanalen.no/frikanalen.no.test.js +++ b/sites/frikanalen.no/frikanalen.no.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=frikanalen.no - -const { parser, url } = require('./frikanalen.no.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'Frikanalen.no' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100' - ) -}) - -it('can parse response', () => { - const content = - '{"count":83,"next":null,"previous":null,"results":[{"id":135605,"video":{"id":626094,"name":"FSCONS 2017 - Keynote: TBA - Linda Sandvik","header":"Linda Sandvik\'s keynote at FSCONS 2017\\r\\n\\r\\nRecorded by NUUG for FSCONS.","description":null,"creator":"davidwnoble@gmail.com","organization":{"id":82,"name":"NUUG","homepage":"https://www.nuug.no/","description":"Forening NUUG er for alle som er interessert i fri programvare, åpne standarder og Unix-lignende operativsystemer.","postalAddress":"","streetAddress":"","editorId":2148,"editorName":"David Noble","editorEmail":"davidwnoble@gmail.com","editorMsisdn":"","fkmember":true},"duration":"00:57:55.640000","categories":["Samfunn"]},"schedulereason":5,"starttime":"2022-01-19T00:47:00+01:00","endtime":"2022-01-19T01:44:55.640000+01:00","duration":"00:57:55.640000"}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-18T23:47:00.000Z', - stop: '2022-01-19T00:44:55.640Z', - title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik', - category: ['Samfunn'], - description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS." - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"count":0,"next":null,"previous":null,"results":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=frikanalen.no + +const { parser, url } = require('./frikanalen.no.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'Frikanalen.no' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://frikanalen.no/api/scheduleitems/?date=2022-01-19&format=json&limit=100' + ) +}) + +it('can parse response', () => { + const content = + '{"count":83,"next":null,"previous":null,"results":[{"id":135605,"video":{"id":626094,"name":"FSCONS 2017 - Keynote: TBA - Linda Sandvik","header":"Linda Sandvik\'s keynote at FSCONS 2017\\r\\n\\r\\nRecorded by NUUG for FSCONS.","description":null,"creator":"davidwnoble@gmail.com","organization":{"id":82,"name":"NUUG","homepage":"https://www.nuug.no/","description":"Forening NUUG er for alle som er interessert i fri programvare, åpne standarder og Unix-lignende operativsystemer.","postalAddress":"","streetAddress":"","editorId":2148,"editorName":"David Noble","editorEmail":"davidwnoble@gmail.com","editorMsisdn":"","fkmember":true},"duration":"00:57:55.640000","categories":["Samfunn"]},"schedulereason":5,"starttime":"2022-01-19T00:47:00+01:00","endtime":"2022-01-19T01:44:55.640000+01:00","duration":"00:57:55.640000"}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-18T23:47:00.000Z', + stop: '2022-01-19T00:44:55.640Z', + title: 'FSCONS 2017 - Keynote: TBA - Linda Sandvik', + category: ['Samfunn'], + description: "Linda Sandvik's keynote at FSCONS 2017\r\n\r\nRecorded by NUUG for FSCONS." + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"count":0,"next":null,"previous":null,"results":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/gatotv.com/gatotv.com.config.js b/sites/gatotv.com/gatotv.com.config.js index 6c7a235e..1ace7d5b 100644 --- a/sites/gatotv.com/gatotv.com.config.js +++ b/sites/gatotv.com/gatotv.com.config.js @@ -1,100 +1,100 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const url = require('url') -const path = require('path') -const { DateTime } = require('luxon') - -module.exports = { - site: 'gatotv.com', - days: 2, - url({ channel, date }) { - return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - date = date.subtract(1, 'd') - items.forEach((item, i) => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (i === 0 && start.hour >= 5) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.gatotv.com/guia_tv/completa') - .then(response => response.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href') - const parsed = url.parse(link) - - return { - lang: 'es', - site_id: path.basename(parsed.pathname), - name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text() - } - }) - } -} - -function parseTitle($item) { - return $item('td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span').text() -} - -function parseDescription($item) { - return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim() -} - -function parseIcon($item) { - return $item('td:nth-child(3) > a > img').attr('src') -} - -function parseStart($item, date) { - const time = $item('td:nth-child(1) > div > time').attr('datetime') - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'EST' - }).toUTC() -} - -function parseStop($item, date) { - const time = $item('td:nth-child(2) > div > time').attr('datetime') - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'EST' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - 'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG' - ) - .find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected') - .toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const url = require('url') +const path = require('path') +const { DateTime } = require('luxon') + +module.exports = { + site: 'gatotv.com', + days: 2, + url({ channel, date }) { + return `https://www.gatotv.com/canal/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + date = date.subtract(1, 'd') + items.forEach((item, i) => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (i === 0 && start.hour >= 5) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.gatotv.com/guia_tv/completa') + .then(response => response.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $('.tbl_EPG_row,.tbl_EPG_rowAlternate').toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const link = $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').attr('href') + const parsed = url.parse(link) + + return { + lang: 'es', + site_id: path.basename(parsed.pathname), + name: $item('td:nth-child(1) > div:nth-child(2) > a:nth-child(3)').text() + } + }) + } +} + +function parseTitle($item) { + return $item('td:nth-child(4) > div > div > a > span,td:nth-child(3) > div > div > span,td:nth-child(3) > div > div > a > span').text() +} + +function parseDescription($item) { + return $item('td:nth-child(4) > div').clone().children().remove().end().text().trim() +} + +function parseIcon($item) { + return $item('td:nth-child(3) > a > img').attr('src') +} + +function parseStart($item, date) { + const time = $item('td:nth-child(1) > div > time').attr('datetime') + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'EST' + }).toUTC() +} + +function parseStop($item, date) { + const time = $item('td:nth-child(2) > div > time').attr('datetime') + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'EST' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + 'body > div.div_content > table:nth-child(8) > tbody > tr:nth-child(2) > td:nth-child(1) > table.tbl_EPG' + ) + .find('.tbl_EPG_row,.tbl_EPG_rowAlternate,.tbl_EPG_row_selected') + .toArray() +} diff --git a/sites/gatotv.com/gatotv.com.test.js b/sites/gatotv.com/gatotv.com.test.js index 5f96e16e..50c8a572 100644 --- a/sites/gatotv.com/gatotv.com.test.js +++ b/sites/gatotv.com/gatotv.com.test.js @@ -1,82 +1,82 @@ -// npm run channels:parse -- --config=./sites/gatotv.com/gatotv.com.config.js --output=./sites/gatotv.com/gatotv.com.channels.xml -// npm run grab -- --site=gatotv.com - -const { parser, url } = require('./gatotv.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'm_0', - xmltv_id: '0porMovistarPlus.es' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8') - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-13T04:30:00.000Z', - stop: '2023-06-13T05:32:00.000Z', - title: 'Supergarcía' - }) - - expect(results[1]).toMatchObject({ - start: '2023-06-13T05:32:00.000Z', - stop: '2023-06-13T06:59:00.000Z', - title: 'La resistencia' - }) - - expect(results[25]).toMatchObject({ - start: '2023-06-14T04:46:00.000Z', - stop: '2023-06-14T05:00:00.000Z', - title: 'Una familia absolutamente normal' - }) -}) - -it('can parse response when the guide starts from midnight', () => { - date = date.add(1, 'd') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8') - const results = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-14T05:00:00.000Z', - stop: '2023-06-14T05:32:00.000Z', - title: 'Ilustres Ignorantes' - }) - - expect(results[26]).toMatchObject({ - start: '2023-06-15T04:30:00.000Z', - stop: '2023-06-15T05:30:00.000Z', - title: 'Showriano' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/gatotv.com/gatotv.com.config.js --output=./sites/gatotv.com/gatotv.com.channels.xml +// npm run grab -- --site=gatotv.com + +const { parser, url } = require('./gatotv.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +let date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'm_0', + xmltv_id: '0porMovistarPlus.es' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.gatotv.com/canal/m_0/2023-06-13') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.html'), 'utf8') + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-13T04:30:00.000Z', + stop: '2023-06-13T05:32:00.000Z', + title: 'Supergarcía' + }) + + expect(results[1]).toMatchObject({ + start: '2023-06-13T05:32:00.000Z', + stop: '2023-06-13T06:59:00.000Z', + title: 'La resistencia' + }) + + expect(results[25]).toMatchObject({ + start: '2023-06-14T04:46:00.000Z', + stop: '2023-06-14T05:00:00.000Z', + title: 'Una familia absolutamente normal' + }) +}) + +it('can parse response when the guide starts from midnight', () => { + date = date.add(1, 'd') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_1.html'), 'utf8') + const results = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-14T05:00:00.000Z', + stop: '2023-06-14T05:32:00.000Z', + title: 'Ilustres Ignorantes' + }) + + expect(results[26]).toMatchObject({ + start: '2023-06-15T04:30:00.000Z', + stop: '2023-06-15T05:30:00.000Z', + title: 'Showriano' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/getafteritmedia.com/getafteritmedia.com.config.js b/sites/getafteritmedia.com/getafteritmedia.com.config.js index 5fdcaed4..a86068cb 100644 --- a/sites/getafteritmedia.com/getafteritmedia.com.config.js +++ b/sites/getafteritmedia.com/getafteritmedia.com.config.js @@ -1,64 +1,64 @@ -const table2array = require('table2array') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const isoWeek = require('dayjs/plugin/isoWeek') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) -dayjs.extend(isoWeek) - -module.exports = { - site: 'getafteritmedia.com', - days: 2, - url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml', - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.time}`, - 'YYYY-MM-DD HH:mm A', - 'America/New_York' - ) -} - -function parseItems(content, channel, date) { - const day = date.isoWeekday() - const $ = cheerio.load(content) - const table = $.html($(`#${channel.site_id} table`)) - let data = table2array(table) - data.splice(0, 5) - - return data.map(row => { - return { - time: row[1], - title: row[day + 1] - } - }) -} +const table2array = require('table2array') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const isoWeek = require('dayjs/plugin/isoWeek') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) +dayjs.extend(isoWeek) + +module.exports = { + site: 'getafteritmedia.com', + days: 2, + url: 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml', + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.time}`, + 'YYYY-MM-DD HH:mm A', + 'America/New_York' + ) +} + +function parseItems(content, channel, date) { + const day = date.isoWeekday() + const $ = cheerio.load(content) + const table = $.html($(`#${channel.site_id} table`)) + let data = table2array(table) + data.splice(0, 5) + + return data.map(row => { + return { + time: row[1], + title: row[day + 1] + } + }) +} diff --git a/sites/getafteritmedia.com/getafteritmedia.com.test.js b/sites/getafteritmedia.com/getafteritmedia.com.test.js index 5d95345f..292caf42 100644 --- a/sites/getafteritmedia.com/getafteritmedia.com.test.js +++ b/sites/getafteritmedia.com/getafteritmedia.com.test.js @@ -1,47 +1,47 @@ -// npm run grab -- --site=getafteritmedia.com - -const { parser, url } = require('./getafteritmedia.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '494637005', - xmltv_id: 'REVNWebFeed.us' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-26T05:00:00.000Z', - stop: '2022-11-26T05:30:00.000Z', - title: 'The Appraisers' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=getafteritmedia.com + +const { parser, url } = require('./getafteritmedia.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '494637005', + xmltv_id: 'REVNWebFeed.us' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://docs.google.com/spreadsheets/d/e/2PACX-1vQcDmb9OnO0HpbjINfGaepqgGTp3VSmPs7hs654n3sRKrq4Q9y6uPSEvVvq9MwTLYG_n_V7vh0rFYP9/pubhtml' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-26T05:00:00.000Z', + stop: '2022-11-26T05:30:00.000Z', + title: 'The Appraisers' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/guidatv.sky.it/guidatv.sky.it.config.js b/sites/guidatv.sky.it/guidatv.sky.it.config.js index d3f5dcf5..eac77e96 100644 --- a/sites/guidatv.sky.it/guidatv.sky.it.config.js +++ b/sites/guidatv.sky.it/guidatv.sky.it.config.js @@ -1,72 +1,72 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'guidatv.sky.it', - days: 2, - url: function ({ date, channel }) { - const [env, id] = channel.site_id.split('#') - return `https://apid.sky.it/gtv/v1/events?from=${date.format('YYYY-MM-DD')}T00:00:00Z&to=${date - .add(1, 'd') - .format('YYYY-MM-DD')}T00:00:00Z&pageSize=999&pageNum=0&env=${env}&channels=${id}` - }, - parser: function ({ content }) { - const programs = [] - const data = JSON.parse(content) - const items = data.events - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.eventTitle, - description: item.eventSynopsis, - category: parseCategory(item), - season: parseSeason(item), - episode: parseEpisode(item), - start: parseStart(item), - stop: parseStop(item), - url: parseURL(item), - icon: parseIcon(item) - }) - }) - - return programs - } -} - -function parseCategory(item) { - let category = item.content.genre.name || null - const subcategory = item.content.subgenre.name || null - if (category && subcategory) { - category += `/${subcategory}` - } - return category -} - -function parseStart(item) { - return item.starttime ? dayjs(item.starttime) : null -} - -function parseStop(item) { - return item.endtime ? dayjs(item.endtime) : null -} - -function parseURL(item) { - return item.content.url ? `https://guidatv.sky.it${item.content.url}` : null -} - -function parseIcon(item) { - const cover = item.content.imagesMap ? item.content.imagesMap.find(i => i.key === 'cover') : null - - return cover && cover.img && cover.img.url ? `https://guidatv.sky.it${cover.img.url}` : null -} - -function parseSeason(item) { - if (!item.content.seasonNumber) return null - if (String(item.content.seasonNumber).length > 2) return null - return item.content.seasonNumber -} - -function parseEpisode(item) { - if (!item.content.episodeNumber) return null - if (String(item.content.episodeNumber).length > 3) return null - return item.content.episodeNumber -} +const dayjs = require('dayjs') + +module.exports = { + site: 'guidatv.sky.it', + days: 2, + url: function ({ date, channel }) { + const [env, id] = channel.site_id.split('#') + return `https://apid.sky.it/gtv/v1/events?from=${date.format('YYYY-MM-DD')}T00:00:00Z&to=${date + .add(1, 'd') + .format('YYYY-MM-DD')}T00:00:00Z&pageSize=999&pageNum=0&env=${env}&channels=${id}` + }, + parser: function ({ content }) { + const programs = [] + const data = JSON.parse(content) + const items = data.events + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.eventTitle, + description: item.eventSynopsis, + category: parseCategory(item), + season: parseSeason(item), + episode: parseEpisode(item), + start: parseStart(item), + stop: parseStop(item), + url: parseURL(item), + icon: parseIcon(item) + }) + }) + + return programs + } +} + +function parseCategory(item) { + let category = item.content.genre.name || null + const subcategory = item.content.subgenre.name || null + if (category && subcategory) { + category += `/${subcategory}` + } + return category +} + +function parseStart(item) { + return item.starttime ? dayjs(item.starttime) : null +} + +function parseStop(item) { + return item.endtime ? dayjs(item.endtime) : null +} + +function parseURL(item) { + return item.content.url ? `https://guidatv.sky.it${item.content.url}` : null +} + +function parseIcon(item) { + const cover = item.content.imagesMap ? item.content.imagesMap.find(i => i.key === 'cover') : null + + return cover && cover.img && cover.img.url ? `https://guidatv.sky.it${cover.img.url}` : null +} + +function parseSeason(item) { + if (!item.content.seasonNumber) return null + if (String(item.content.seasonNumber).length > 2) return null + return item.content.seasonNumber +} + +function parseEpisode(item) { + if (!item.content.episodeNumber) return null + if (String(item.content.episodeNumber).length > 3) return null + return item.content.episodeNumber +} diff --git a/sites/guidatv.sky.it/guidatv.sky.it.test.js b/sites/guidatv.sky.it/guidatv.sky.it.test.js index 77e5ee23..8ff526ca 100644 --- a/sites/guidatv.sky.it/guidatv.sky.it.test.js +++ b/sites/guidatv.sky.it/guidatv.sky.it.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=guidatv.sky.it - -const { parser, url } = require('./guidatv.sky.it.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'DTH#10458', - xmltv_id: '20Mediaset.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458' - ) -}) - -it('can parse response', () => { - const content = - '{"events": [ { "channel": { "id": 10458, "logo": "/logo/545820mediasethd_Light_Fit.png", "logoPadding": "/logo/545820mediasethd_Light_Padding.png", "logoDark": "/logo/545820mediasethd_Dark_Fit.png", "logoDarkPadding": "/logo/545820mediasethd_Dark_Padding.png", "logoLight": "/logo/545820mediasethd_Light_Padding.png", "name": "20Mediaset HD", "number": 151, "category": { "id": 3, "name": "Intrattenimento" } }, "content": { "uuid": "77c630aa-4744-44cb-a88e-3e871c6b73d9", "contentTitle": "Distretto di Polizia", "episodeNumber": 26, "seasonNumber": 6, "url": "/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9", "genre": { "id": 1, "name": "Intrattenimento" }, "subgenre": { "id": 9, "name": "Fiction" }, "imagesMap": [ { "key": "background", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/background?md5ChecksumParam=88d3f48ce855316f4be25ab9bb846d32" } }, { "key": "cover", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b" } }, { "key": "scene", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/16-9?md5ChecksumParam=f41bfe414bec32505abdab19d00b8b43" } } ] }, "eventId": "139585132", "starttime": "2022-05-06T00:35:40Z", "endtime": "2022-05-06T01:15:40Z", "eventTitle": "Distretto di Polizia", "eventSynopsis": "S6 Ep26 La resa dei conti - Fino all\'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e\' tutto come sembrava.", "epgEventTitle": "S6 Ep26 - Distretto di Polizia", "primeVision": false, "resolutions": [ { "resolutionType": "resolution4k", "value": false } ] }]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-06T00:35:40.000Z', - stop: '2022-05-06T01:15:40.000Z', - title: 'Distretto di Polizia', - description: - "S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.", - season: 6, - episode: 26, - icon: 'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b', - category: 'Intrattenimento/Fiction', - url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"events":[],"total":0}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=guidatv.sky.it + +const { parser, url } = require('./guidatv.sky.it.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'DTH#10458', + xmltv_id: '20Mediaset.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://apid.sky.it/gtv/v1/events?from=2022-05-06T00:00:00Z&to=2022-05-07T00:00:00Z&pageSize=999&pageNum=0&env=DTH&channels=10458' + ) +}) + +it('can parse response', () => { + const content = + '{"events": [ { "channel": { "id": 10458, "logo": "/logo/545820mediasethd_Light_Fit.png", "logoPadding": "/logo/545820mediasethd_Light_Padding.png", "logoDark": "/logo/545820mediasethd_Dark_Fit.png", "logoDarkPadding": "/logo/545820mediasethd_Dark_Padding.png", "logoLight": "/logo/545820mediasethd_Light_Padding.png", "name": "20Mediaset HD", "number": 151, "category": { "id": 3, "name": "Intrattenimento" } }, "content": { "uuid": "77c630aa-4744-44cb-a88e-3e871c6b73d9", "contentTitle": "Distretto di Polizia", "episodeNumber": 26, "seasonNumber": 6, "url": "/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9", "genre": { "id": 1, "name": "Intrattenimento" }, "subgenre": { "id": 9, "name": "Fiction" }, "imagesMap": [ { "key": "background", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/background?md5ChecksumParam=88d3f48ce855316f4be25ab9bb846d32" } }, { "key": "cover", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b" } }, { "key": "scene", "img": { "url": "/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/16-9?md5ChecksumParam=f41bfe414bec32505abdab19d00b8b43" } } ] }, "eventId": "139585132", "starttime": "2022-05-06T00:35:40Z", "endtime": "2022-05-06T01:15:40Z", "eventTitle": "Distretto di Polizia", "eventSynopsis": "S6 Ep26 La resa dei conti - Fino all\'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e\' tutto come sembrava.", "epgEventTitle": "S6 Ep26 - Distretto di Polizia", "primeVision": false, "resolutions": [ { "resolutionType": "resolution4k", "value": false } ] }]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-06T00:35:40.000Z', + stop: '2022-05-06T01:15:40.000Z', + title: 'Distretto di Polizia', + description: + "S6 Ep26 La resa dei conti - Fino all'ultimo la sfida tra Ardenzi e Carrano, nemici di vecchia data, riserva clamorosi colpi di scena. E si scopre che non e' tutto come sembrava.", + season: 6, + episode: 26, + icon: 'https://guidatv.sky.it/uuid/77c630aa-4744-44cb-a88e-3e871c6b73d9/cover?md5ChecksumParam=61135b999a63e3d3f4a933b9edeb0c1b', + category: 'Intrattenimento/Fiction', + url: 'https://guidatv.sky.it/serie-tv/distretto-di-polizia/stagione-6/episodio-26/77c630aa-4744-44cb-a88e-3e871c6b73d9' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"events":[],"total":0}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/guide.dstv.com/guide.dstv.com.config.js b/sites/guide.dstv.com/guide.dstv.com.config.js index 44ca692a..212cf8cc 100644 --- a/sites/guide.dstv.com/guide.dstv.com.config.js +++ b/sites/guide.dstv.com/guide.dstv.com.config.js @@ -1,94 +1,94 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -// ERR: certificate has expired -module.exports = { - site: 'guide.dstv.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000, - interpretHeader: false - } - }, - url({ channel, date }) { - const [bouquetId] = channel.site_id.split('#') - - return `https://guide.dstv.com/api/gridview/page?bouquetId=${bouquetId}&genre=all&date=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser({ content, date, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } else if (start.hour() > 12) { - start = start.subtract(1, 'd') - date = date.subtract(1, 'd') - } - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - }, - async channels({ bouquet }) { - const data = await axios - .get( - `https://guide.dstv.com/api/channel/fetchChannelsByGenresInBouquet?bouquetId=${bouquet}&genre=all` - ) - .then(r => r.data) - .catch(console.log) - - const items = data.items - return items.map(item => { - return { - lang: 'en', - site_id: `${bouquet}#${item.channelTag}`, - name: item.channelName - } - }) - } -} - -function parseStart(item, date) { - const time = `${date.format('MM/DD/YYYY')} ${item.time}` - - return dayjs.utc(time, 'MM/DD/YYYY HH:mm') -} - -function parseItems(content, channel) { - const [, channelTag] = channel.site_id.split('#') - const data = JSON.parse(content) - const html = data[channelTag] - if (!html) return [] - const $ = cheerio.load(html) - - return $('li') - .map((i, el) => { - return { - time: $(el).find('.event-time').text().trim(), - title: $(el).find('.event-title').text().trim() - } - }) - .toArray() - .filter(i => i.time && i.title) -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +// ERR: certificate has expired +module.exports = { + site: 'guide.dstv.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000, + interpretHeader: false + } + }, + url({ channel, date }) { + const [bouquetId] = channel.site_id.split('#') + + return `https://guide.dstv.com/api/gridview/page?bouquetId=${bouquetId}&genre=all&date=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser({ content, date, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } else if (start.hour() > 12) { + start = start.subtract(1, 'd') + date = date.subtract(1, 'd') + } + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + }, + async channels({ bouquet }) { + const data = await axios + .get( + `https://guide.dstv.com/api/channel/fetchChannelsByGenresInBouquet?bouquetId=${bouquet}&genre=all` + ) + .then(r => r.data) + .catch(console.log) + + const items = data.items + return items.map(item => { + return { + lang: 'en', + site_id: `${bouquet}#${item.channelTag}`, + name: item.channelName + } + }) + } +} + +function parseStart(item, date) { + const time = `${date.format('MM/DD/YYYY')} ${item.time}` + + return dayjs.utc(time, 'MM/DD/YYYY HH:mm') +} + +function parseItems(content, channel) { + const [, channelTag] = channel.site_id.split('#') + const data = JSON.parse(content) + const html = data[channelTag] + if (!html) return [] + const $ = cheerio.load(html) + + return $('li') + .map((i, el) => { + return { + time: $(el).find('.event-time').text().trim(), + title: $(el).find('.event-title').text().trim() + } + }) + .toArray() + .filter(i => i.time && i.title) +} diff --git a/sites/guide.dstv.com/guide.dstv.com.test.js b/sites/guide.dstv.com/guide.dstv.com.test.js index 09b242e5..4e4f18f1 100644 --- a/sites/guide.dstv.com/guide.dstv.com.test.js +++ b/sites/guide.dstv.com/guide.dstv.com.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=guide.dstv.com -// npm run channels:parse -- --config=./sites/guide.dstv.com/guide.dstv.com.config.js --output=./sites/guide.dstv.com/guide.dstv.com.channels.xml --set=bouquet:c35aaecd-5dd1-480b-ae24-357e600a0e4d - -const { parser, url } = require('./guide.dstv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'b0dc42b8-c651-4c3c-8713-a7fcd04744ee#M4H', - xmltv_id: 'MNetMovies4.za' -} - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://guide.dstv.com/api/gridview/page?bouquetId=b0dc42b8-c651-4c3c-8713-a7fcd04744ee&genre=all&date=2021-11-24' - ) -}) - -it('can parse response', () => { - const content = - "{\"M4H\": \"
  • 21:30

    Deadly Flight

  • 08:25

    I Still Believe

  • 15:50

    Despicable Me

  • 20:35

    The Foreigner

  • \"}" - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-23T21:30:00.000Z', - stop: '2021-11-24T08:25:00.000Z', - title: 'Deadly Flight' - }, - { - start: '2021-11-24T08:25:00.000Z', - stop: '2021-11-24T15:50:00.000Z', - title: 'I Still Believe' - }, - { - start: '2021-11-24T15:50:00.000Z', - stop: '2021-11-24T20:35:00.000Z', - title: 'Despicable Me' - }, - { - start: '2021-11-24T20:35:00.000Z', - stop: '2021-11-24T21:35:00.000Z', - title: 'The Foreigner' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{}' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=guide.dstv.com +// npm run channels:parse -- --config=./sites/guide.dstv.com/guide.dstv.com.config.js --output=./sites/guide.dstv.com/guide.dstv.com.channels.xml --set=bouquet:c35aaecd-5dd1-480b-ae24-357e600a0e4d + +const { parser, url } = require('./guide.dstv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'b0dc42b8-c651-4c3c-8713-a7fcd04744ee#M4H', + xmltv_id: 'MNetMovies4.za' +} + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://guide.dstv.com/api/gridview/page?bouquetId=b0dc42b8-c651-4c3c-8713-a7fcd04744ee&genre=all&date=2021-11-24' + ) +}) + +it('can parse response', () => { + const content = + "{\"M4H\": \"
  • 21:30

    Deadly Flight

  • 08:25

    I Still Believe

  • 15:50

    Despicable Me

  • 20:35

    The Foreigner

  • \"}" + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-23T21:30:00.000Z', + stop: '2021-11-24T08:25:00.000Z', + title: 'Deadly Flight' + }, + { + start: '2021-11-24T08:25:00.000Z', + stop: '2021-11-24T15:50:00.000Z', + title: 'I Still Believe' + }, + { + start: '2021-11-24T15:50:00.000Z', + stop: '2021-11-24T20:35:00.000Z', + title: 'Despicable Me' + }, + { + start: '2021-11-24T20:35:00.000Z', + stop: '2021-11-24T21:35:00.000Z', + title: 'The Foreigner' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/hd-plus.de/hd-plus.de.config.js b/sites/hd-plus.de/hd-plus.de.config.js index 9b960271..dd113878 100644 --- a/sites/hd-plus.de/hd-plus.de.config.js +++ b/sites/hd-plus.de/hd-plus.de.config.js @@ -1,57 +1,57 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'hd-plus.de', - days: 2, - url({ date, channel }) { - const today = dayjs().utc().startOf('d') - const day = date.diff(today, 'd') - - return `https://www.hd-plus.de/epg/channel/${channel.site_id}?d=${day}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ title: parseTitle($item), start, stop }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('td:nth-child(2)').text().split(' ').pop() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Berlin') -} - -function parseTitle($item) { - return $item('td:nth-child(1) > a').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'hd-plus.de', + days: 2, + url({ date, channel }) { + const today = dayjs().utc().startOf('d') + const day = date.diff(today, 'd') + + return `https://www.hd-plus.de/epg/channel/${channel.site_id}?d=${day}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ title: parseTitle($item), start, stop }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('td:nth-child(2)').text().split(' ').pop() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Berlin') +} + +function parseTitle($item) { + return $item('td:nth-child(1) > a').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table > tbody > tr').toArray() +} diff --git a/sites/hd-plus.de/hd-plus.de.test.js b/sites/hd-plus.de/hd-plus.de.test.js index d8cb96d4..ab141969 100644 --- a/sites/hd-plus.de/hd-plus.de.test.js +++ b/sites/hd-plus.de/hd-plus.de.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=hd-plus.de - -const { parser, url } = require('./hd-plus.de.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1-2-3-tv-hd', - xmltv_id: '123tv.de' -} -const content = - '
    1-2-3.tv HD

    1-2-3.tv HD

    Titel Ausstrahlungszeit
    Ihre Lieblingsuhren Do 25.11 00:00
    Ihre Lieblingsuhren Do 25.11 01:00
    Flash DealsDo 25.11 06:00
    ' - -it('can generate valid url', () => { - const today = dayjs.utc().startOf('d') - expect(url({ channel, date: today })).toBe('https://www.hd-plus.de/epg/channel/1-2-3-tv-hd?d=0') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T00:00:00.000Z', - title: 'Ihre Lieblingsuhren' - }, - { - start: '2021-11-25T00:00:00.000Z', - stop: '2021-11-25T05:00:00.000Z', - title: 'Ihre Lieblingsuhren' - }, - { - start: '2021-11-25T05:00:00.000Z', - stop: '2021-11-25T06:00:00.000Z', - title: 'Flash Deals' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=hd-plus.de + +const { parser, url } = require('./hd-plus.de.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1-2-3-tv-hd', + xmltv_id: '123tv.de' +} +const content = + '
    1-2-3.tv HD

    1-2-3.tv HD

    Titel Ausstrahlungszeit
    Ihre Lieblingsuhren Do 25.11 00:00
    Ihre Lieblingsuhren Do 25.11 01:00
    Flash DealsDo 25.11 06:00
    ' + +it('can generate valid url', () => { + const today = dayjs.utc().startOf('d') + expect(url({ channel, date: today })).toBe('https://www.hd-plus.de/epg/channel/1-2-3-tv-hd?d=0') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'Ihre Lieblingsuhren' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T05:00:00.000Z', + title: 'Ihre Lieblingsuhren' + }, + { + start: '2021-11-25T05:00:00.000Z', + stop: '2021-11-25T06:00:00.000Z', + title: 'Flash Deals' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/horizon.tv/horizon.tv.config.js b/sites/horizon.tv/horizon.tv.config.js index 0444317c..225021ca 100644 --- a/sites/horizon.tv/horizon.tv.config.js +++ b/sites/horizon.tv/horizon.tv.config.js @@ -1,145 +1,145 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web' - -module.exports = { - site: 'horizon.tv', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const promises = [ - axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) - ] - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - }) - .catch(console.error) - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.t, - description: parseDescription(detail), - category: parseCategory(detail), - season: parseSeason(detail), - episode: parseEpisode(detail), - actors: parseActors(detail), - directors: parseDirectors(detail), - date: parseYear(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/channels`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'sk', - site_id: item.id.replace('lgi-obolite-sk-prod-master:5-', ''), - name: item.title - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.i) return {} - const url = `${API_ENDPOINT}/listings/${item.i}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseStart(item) { - return dayjs(item.s) -} - -function parseStop(item) { - return dayjs(item.e) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = typeof content === 'string' ? JSON.parse(content) : content - if (!data || !Array.isArray(data.entries)) return [] - const entity = data.entries.find(e => e.o === `lgi-obolite-sk-prod-master:${channel.site_id}`) - return entity ? entity.l : [] -} - -function parseDescription(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.longDescription || null -} - -function parseCategory(detail) { - if (!detail) return [] - if (!detail.program) return [] - if (!detail.program.categories) return [] - let categories = [] - detail.program.categories.forEach(category => { - categories.push(category.title) - }) - return categories -} - -function parseSeason(detail) { - if (!detail) return null - if (!detail.program) return null - if (!detail.program.seriesNumber) return null - if (String(detail.program.seriesNumber).length > 2) return null - return detail.program.seriesNumber -} - -function parseEpisode(detail) { - if (!detail) return null - if (!detail.program) return null - if (!detail.program.seriesEpisodeNumber) return null - if (String(detail.program.seriesEpisodeNumber).length > 3) return null - return detail.program.seriesEpisodeNumber -} - -function parseDirectors(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.directors || [] -} - -function parseActors(detail) { - if (!detail) return [] - if (!detail.program) return [] - return detail.program.cast || [] -} - -function parseYear(detail) { - if (!detail) return null - if (!detail.program) return null - return detail.program.year || null -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web' + +module.exports = { + site: 'horizon.tv', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const promises = [ + axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) + ] + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + }) + .catch(console.error) + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.t, + description: parseDescription(detail), + category: parseCategory(detail), + season: parseSeason(detail), + episode: parseEpisode(detail), + actors: parseActors(detail), + directors: parseDirectors(detail), + date: parseYear(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/channels`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'sk', + site_id: item.id.replace('lgi-obolite-sk-prod-master:5-', ''), + name: item.title + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.i) return {} + const url = `${API_ENDPOINT}/listings/${item.i}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseStart(item) { + return dayjs(item.s) +} + +function parseStop(item) { + return dayjs(item.e) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = typeof content === 'string' ? JSON.parse(content) : content + if (!data || !Array.isArray(data.entries)) return [] + const entity = data.entries.find(e => e.o === `lgi-obolite-sk-prod-master:${channel.site_id}`) + return entity ? entity.l : [] +} + +function parseDescription(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.longDescription || null +} + +function parseCategory(detail) { + if (!detail) return [] + if (!detail.program) return [] + if (!detail.program.categories) return [] + let categories = [] + detail.program.categories.forEach(category => { + categories.push(category.title) + }) + return categories +} + +function parseSeason(detail) { + if (!detail) return null + if (!detail.program) return null + if (!detail.program.seriesNumber) return null + if (String(detail.program.seriesNumber).length > 2) return null + return detail.program.seriesNumber +} + +function parseEpisode(detail) { + if (!detail) return null + if (!detail.program) return null + if (!detail.program.seriesEpisodeNumber) return null + if (String(detail.program.seriesEpisodeNumber).length > 3) return null + return detail.program.seriesEpisodeNumber +} + +function parseDirectors(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.directors || [] +} + +function parseActors(detail) { + if (!detail) return [] + if (!detail.program) return [] + return detail.program.cast || [] +} + +function parseYear(detail) { + if (!detail) return null + if (!detail.program) return null + return detail.program.year || null +} diff --git a/sites/horizon.tv/horizon.tv.test.js b/sites/horizon.tv/horizon.tv.test.js index 4a5a6758..ccf6af4a 100644 --- a/sites/horizon.tv/horizon.tv.test.js +++ b/sites/horizon.tv/horizon.tv.test.js @@ -1,266 +1,266 @@ -// npm run channels:parse -- --config=./sites/horizon.tv/horizon.tv.config.js --output=./sites/horizon.tv/horizon.tv.channels.xml -// npm run grab -- --site=horizon.tv - -const { parser, url } = require('./horizon.tv.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-02-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '10024', - xmltv_id: 'AMCCzechRepublic.cz' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1' - ) -}) - -it('can parse response', done => { - const content = - '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791343825,"title":"EPG","periods":4,"periodStartTime":1675724400000,"periodEndTime":1675746000000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","t":"Avengement","s":1675719300000,"e":1675724700000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - - axios.get.mockImplementation(url => { - if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791376097,"title":"EPG","periods":4,"periodStartTime":1675746000000,"periodEndTime":1675767600000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","t":"Zoom In","s":1675744500000,"e":1675746000000,"c":"lgi-obolite-sk-prod-master:genre-21","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675791024984,"title":"EPG","periods":4,"periodStartTime":1675767600000,"periodEndTime":1675789200000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","t":"Studentka","s":1675761000000,"e":1675767600000,"c":"lgi-obolite-sk-prod-master:genre-14","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675790973469,"title":"EPG","periods":4,"periodStartTime":1675789200000,"periodEndTime":1675810800000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","t":"Zilionáři","s":1675785900000,"e":1675791900000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","startTime":1675719300000,"endTime":1675724700000,"actualStartTime":1675719300000,"actualEndTime":1675724700000,"expirationDate":1676324100000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","scCridImi":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","program":{"id":"crid:~~2F~~2Fport.cs~~2F122941980","title":"Avengement","description":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","longDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"18","cast":["Scott Adkins","Craig Fairbrass","Thomas Turgoose","Nick Moran","Kierston Wareing","Leo Gregory","Mark Strange","Luke LaFontaine","Beau Fowler","Dan Styles","Christopher Sciueref","Matt Routledge","Jane Thorne","Louis Mandylor","Terence Maynard","Greg Burridge","Michael Higgs","Damian Gallagher","Daniel Adegboyega","John Ioannou","Sofie Golding-Spittle","Joe Egan","Darren Swain","Lee Charles","Dominic Kinnaird","Ross O\'Hennessy","Teresa Mahoney","Andrew Dunkelberger","Sam Hardy","Ivan Moy","Mark Sears","Phillip Ray Tommy"],"directors":["Jesse V. Johnson"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_3fa8387df870473fdacb1024635b52b2496b159c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_19e3a660e637cd39e31046c284a66b3a95d698e4.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","shortDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","mediaType":"FeatureFilm","year":"2019","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676247300000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","startTime":1675744500000,"endTime":1675746000000,"actualStartTime":1675744500000,"actualEndTime":1675746000000,"expirationDate":1676349300000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:e85129f9d1e211406a521df7a36f22237c22651b","scCridImi":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","program":{"id":"crid:~~2F~~2Fport.cs~~2F248281986","title":"Zoom In","description":"Film/Kino","longDescription":"Film/Kino","medium":"TV","categories":[{"id":"lgi-obolite-sk-prod-master:genre-21","title":"Hudba a umenie","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":[],"directors":[],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_cbed64b557e83227a2292604cbcae2d193877b1c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=180&h=260&mode=box"}],"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","shortDescription":"Film/Kino","mediaType":"Episode","year":"2010","seriesEpisodeNumber":"1302070535","seriesNumber":"1302080520","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675746000000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","startTime":1675761000000,"endTime":1675767600000,"actualStartTime":1675761000000,"actualEndTime":1675767600000,"expirationDate":1676365800000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","scCridImi":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","program":{"id":"crid:~~2F~~2Fport.cs~~2F1379541","title":"Studentka","description":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","longDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-4","title":"Komédia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":["Sophie Marceauová","Vincent Lindon","Elisabeth Vitali","Elena Pompei","Jean-Claude Leguay","Brigitte Chamarande","Christian Pereira","Gérard Dacier","Roberto Attias","Beppe Chierici","Nathalie Mann","Anne Macina","Janine Souchon","Virginie Demians","Hugues Leforestier","Jacqueline Noëlle","Marc-André Brunet","Isabelle Caubère","André Chazel","Med Salah Cheurfi","Guillaume Corea","Eric Denize","Gilles Gaston-Dreyfuss","Benoît Gourley","Marc Innocenti","Najim Laouriga","Laurent Ledermann","Philippe Maygal","Dominique Pifarely","Ysé Tran"],"directors":["Francis De Gueltz","Dominique Talmon","Claude Pinoteau"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_a8abceaa59bbb0aae8031dcdd5deba03aba8a100.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","shortDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","mediaType":"FeatureFilm","year":"1988","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675767600000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else if ( - url === - 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","startTime":1675785900000,"endTime":1675791900000,"actualStartTime":1675785900000,"actualEndTime":1675791900000,"expirationDate":1676390700000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","scCridImi":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","program":{"id":"crid:~~2F~~2Fport.cs~~2F71927954","title":"Zilionáři","description":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","longDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"15","cast":["Zach Galifianakis","Kristen Wiigová","Owen Wilson","Kate McKinnon","Leslie Jones","Jason Sudeikis","Ross Kimball","Devin Ratray","Mary Elizabeth Ellisová","Jon Daly","Ken Marino","Daniel Zacapa","Tom Werme","Njema Williams","Nils Cruz","Michael Fraguada","Christian Gonzalez","Candace Blanchard","Karsten Friske","Dallas Edwards","Barry Ratcliffe","Shelton Grant","Laura Palka","Reegus Flenory","Wynn Reichert","Jill Jane Clements","Joseph S. Wilson","Jee An","Rhoda Griffisová","Nicole Dupre Sobchack"],"directors":["Scott August","Richard L. Fox","Michelle Malley-Campos","Sebastian Mazzola","Steven Ritzi","Pete Waterman","Jared Hess"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_fd098116bac1429318aaf5fdae498ce76e258782.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_6f857ae9375b3bcceb6353a5b35775f52cd85302.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","shortDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","mediaType":"FeatureFilm","year":"2016","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676187900000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' - ) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2023-02-06T21:35:00.000Z', - stop: '2023-02-06T23:05:00.000Z', - title: 'Avengement', - description: - 'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.', - category: ['Drama', 'Akcia'], - directors: ['Jesse V. Johnson'], - actors: [ - 'Scott Adkins', - 'Craig Fairbrass', - 'Thomas Turgoose', - 'Nick Moran', - 'Kierston Wareing', - 'Leo Gregory', - 'Mark Strange', - 'Luke LaFontaine', - 'Beau Fowler', - 'Dan Styles', - 'Christopher Sciueref', - 'Matt Routledge', - 'Jane Thorne', - 'Louis Mandylor', - 'Terence Maynard', - 'Greg Burridge', - 'Michael Higgs', - 'Damian Gallagher', - 'Daniel Adegboyega', - 'John Ioannou', - 'Sofie Golding-Spittle', - 'Joe Egan', - 'Darren Swain', - 'Lee Charles', - 'Dominic Kinnaird', - "Ross O'Hennessy", - 'Teresa Mahoney', - 'Andrew Dunkelberger', - 'Sam Hardy', - 'Ivan Moy', - 'Mark Sears', - 'Phillip Ray Tommy' - ], - date: '2019' - }, - { - start: '2023-02-07T04:35:00.000Z', - stop: '2023-02-07T05:00:00.000Z', - title: 'Zoom In', - description: 'Film/Kino', - category: ['Hudba a umenie', 'Film'], - date: '2010' - }, - { - start: '2023-02-07T09:10:00.000Z', - stop: '2023-02-07T11:00:00.000Z', - title: 'Studentka', - description: - 'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?', - category: ['Film', 'Komédia'], - actors: [ - 'Sophie Marceauová', - 'Vincent Lindon', - 'Elisabeth Vitali', - 'Elena Pompei', - 'Jean-Claude Leguay', - 'Brigitte Chamarande', - 'Christian Pereira', - 'Gérard Dacier', - 'Roberto Attias', - 'Beppe Chierici', - 'Nathalie Mann', - 'Anne Macina', - 'Janine Souchon', - 'Virginie Demians', - 'Hugues Leforestier', - 'Jacqueline Noëlle', - 'Marc-André Brunet', - 'Isabelle Caubère', - 'André Chazel', - 'Med Salah Cheurfi', - 'Guillaume Corea', - 'Eric Denize', - 'Gilles Gaston-Dreyfuss', - 'Benoît Gourley', - 'Marc Innocenti', - 'Najim Laouriga', - 'Laurent Ledermann', - 'Philippe Maygal', - 'Dominique Pifarely', - 'Ysé Tran' - ], - directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'], - date: '1988' - }, - { - start: '2023-02-07T16:05:00.000Z', - stop: '2023-02-07T17:45:00.000Z', - title: 'Zilionáři', - description: - 'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...', - category: ['Drama', 'Akcia'], - actors: [ - 'Zach Galifianakis', - 'Kristen Wiigová', - 'Owen Wilson', - 'Kate McKinnon', - 'Leslie Jones', - 'Jason Sudeikis', - 'Ross Kimball', - 'Devin Ratray', - 'Mary Elizabeth Ellisová', - 'Jon Daly', - 'Ken Marino', - 'Daniel Zacapa', - 'Tom Werme', - 'Njema Williams', - 'Nils Cruz', - 'Michael Fraguada', - 'Christian Gonzalez', - 'Candace Blanchard', - 'Karsten Friske', - 'Dallas Edwards', - 'Barry Ratcliffe', - 'Shelton Grant', - 'Laura Palka', - 'Reegus Flenory', - 'Wynn Reichert', - 'Jill Jane Clements', - 'Joseph S. Wilson', - 'Jee An', - 'Rhoda Griffisová', - 'Nicole Dupre Sobchack' - ], - directors: [ - 'Scott August', - 'Richard L. Fox', - 'Michelle Malley-Campos', - 'Sebastian Mazzola', - 'Steven Ritzi', - 'Pete Waterman', - 'Jared Hess' - ], - date: '2016' - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', - channel, - date - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/horizon.tv/horizon.tv.config.js --output=./sites/horizon.tv/horizon.tv.channels.xml +// npm run grab -- --site=horizon.tv + +const { parser, url } = require('./horizon.tv.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-02-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '10024', + xmltv_id: 'AMCCzechRepublic.cz' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/1' + ) +}) + +it('can parse response', done => { + const content = + '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791343825,"title":"EPG","periods":4,"periodStartTime":1675724400000,"periodEndTime":1675746000000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","t":"Avengement","s":1675719300000,"e":1675724700000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + + axios.get.mockImplementation(url => { + if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/2' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":184,"totalResults":184,"updated":1675790518889,"expires":1675791376097,"title":"EPG","periods":4,"periodStartTime":1675746000000,"periodEndTime":1675767600000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","t":"Zoom In","s":1675744500000,"e":1675746000000,"c":"lgi-obolite-sk-prod-master:genre-21","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/3' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675791024984,"title":"EPG","periods":4,"periodStartTime":1675767600000,"periodEndTime":1675789200000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","t":"Studentka","s":1675761000000,"e":1675767600000,"c":"lgi-obolite-sk-prod-master:genre-14","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/programschedules/20230207/4' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":184,"totalResults":184,"updated":1675789948804,"expires":1675790973469,"title":"EPG","periods":4,"periodStartTime":1675789200000,"periodEndTime":1675810800000,"entries":[{"o":"lgi-obolite-sk-prod-master:10024","l":[{"i":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","t":"Zilionáři","s":1675785900000,"e":1675791900000,"c":"lgi-obolite-sk-prod-master:genre-9","a":false,"r":true,"rm":true,"rs":0,"re":604800,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","startTime":1675719300000,"endTime":1675724700000,"actualStartTime":1675719300000,"actualEndTime":1675724700000,"expirationDate":1676324100000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","scCridImi":"crid:~~2F~~2Fport.cs~~2F122941980,imi:7ca159c917344e0dd3fbe1cd8db5ff8043d96a78","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","program":{"id":"crid:~~2F~~2Fport.cs~~2F122941980","title":"Avengement","description":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","longDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"18","cast":["Scott Adkins","Craig Fairbrass","Thomas Turgoose","Nick Moran","Kierston Wareing","Leo Gregory","Mark Strange","Luke LaFontaine","Beau Fowler","Dan Styles","Christopher Sciueref","Matt Routledge","Jane Thorne","Louis Mandylor","Terence Maynard","Greg Burridge","Michael Higgs","Damian Gallagher","Daniel Adegboyega","John Ioannou","Sofie Golding-Spittle","Joe Egan","Darren Swain","Lee Charles","Dominic Kinnaird","Ross O\'Hennessy","Teresa Mahoney","Andrew Dunkelberger","Sam Hardy","Ivan Moy","Mark Sears","Phillip Ray Tommy"],"directors":["Jesse V. Johnson"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_3fa8387df870473fdacb1024635b52b2496b159c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_19e3a660e637cd39e31046c284a66b3a95d698e4.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_939160772e45a783fb3a19970696f5ebcb6e568b.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F122941980","shortDescription":"Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za...","mediaType":"FeatureFilm","year":"2019","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F122941980","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676247300000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","startTime":1675744500000,"endTime":1675746000000,"actualStartTime":1675744500000,"actualEndTime":1675746000000,"expirationDate":1676349300000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:e85129f9d1e211406a521df7a36f22237c22651b","scCridImi":"crid:~~2F~~2Fport.cs~~2F248281986,imi:e85129f9d1e211406a521df7a36f22237c22651b","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","program":{"id":"crid:~~2F~~2Fport.cs~~2F248281986","title":"Zoom In","description":"Film/Kino","longDescription":"Film/Kino","medium":"TV","categories":[{"id":"lgi-obolite-sk-prod-master:genre-21","title":"Hudba a umenie","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":[],"directors":[],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_cbed64b557e83227a2292604cbcae2d193877b1c.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_cfe405e669385365846b69196e1e94caa3e60de0.jpg?w=180&h=260&mode=box"}],"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F41764266","shortDescription":"Film/Kino","mediaType":"Episode","year":"2010","seriesEpisodeNumber":"1302070535","seriesNumber":"1302080520","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fport.cs~~2F41764266_series","rootId":"crid:~~2F~~2Fport.cs~~2F41764266","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675746000000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","startTime":1675761000000,"endTime":1675767600000,"actualStartTime":1675761000000,"actualEndTime":1675767600000,"expirationDate":1676365800000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","scCridImi":"crid:~~2F~~2Fport.cs~~2F1379541,imi:5f806a2a0bc13e9745e14907a27116c60ea2c6ad","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","program":{"id":"crid:~~2F~~2Fport.cs~~2F1379541","title":"Studentka","description":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","longDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-14","title":"Film","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-4","title":"Komédia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"9","cast":["Sophie Marceauová","Vincent Lindon","Elisabeth Vitali","Elena Pompei","Jean-Claude Leguay","Brigitte Chamarande","Christian Pereira","Gérard Dacier","Roberto Attias","Beppe Chierici","Nathalie Mann","Anne Macina","Janine Souchon","Virginie Demians","Hugues Leforestier","Jacqueline Noëlle","Marc-André Brunet","Isabelle Caubère","André Chazel","Med Salah Cheurfi","Guillaume Corea","Eric Denize","Gilles Gaston-Dreyfuss","Benoît Gourley","Marc Innocenti","Najim Laouriga","Laurent Ledermann","Philippe Maygal","Dominique Pifarely","Ysé Tran"],"directors":["Francis De Gueltz","Dominique Talmon","Claude Pinoteau"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_a8abceaa59bbb0aae8031dcdd5deba03aba8a100.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_72b11621270454812ac8474698fc75670db4a49d.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F1379541","shortDescription":"Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný...","mediaType":"FeatureFilm","year":"1988","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F1379541","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1675767600000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else if ( + url === + 'https://legacy-static.oesp.horizon.tv/oesp/v4/SK/slk/web/listings/crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","startTime":1675785900000,"endTime":1675791900000,"actualStartTime":1675785900000,"actualEndTime":1675791900000,"expirationDate":1676390700000,"stationId":"lgi-obolite-sk-prod-master:10024","imi":"imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","scCridImi":"crid:~~2F~~2Fport.cs~~2F71927954,imi:f1b4b0285b72cf44cba74e1c62322a4c682385c7","mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","program":{"id":"crid:~~2F~~2Fport.cs~~2F71927954","title":"Zilionáři","description":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","longDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...","medium":"Movie","categories":[{"id":"lgi-obolite-sk-prod-master:genre-9","title":"Drama","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-obolite-sk-prod-master:genre-33","title":"Akcia","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"parentalRating":"15","cast":["Zach Galifianakis","Kristen Wiigová","Owen Wilson","Kate McKinnon","Leslie Jones","Jason Sudeikis","Ross Kimball","Devin Ratray","Mary Elizabeth Ellisová","Jon Daly","Ken Marino","Daniel Zacapa","Tom Werme","Njema Williams","Nils Cruz","Michael Fraguada","Christian Gonzalez","Candace Blanchard","Karsten Friske","Dallas Edwards","Barry Ratcliffe","Shelton Grant","Laura Palka","Reegus Flenory","Wynn Reichert","Jill Jane Clements","Joseph S. Wilson","Jee An","Rhoda Griffisová","Nicole Dupre Sobchack"],"directors":["Scott August","Richard L. Fox","Michelle Malley-Campos","Sebastian Mazzola","Steven Ritzi","Pete Waterman","Jared Hess"],"images":[{"assetType":"HighResLandscape","assetTypes":["HighResLandscape"],"url":"http://62.179.125.152/SK/Images/hrl_fd098116bac1429318aaf5fdae498ce76e258782.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"http://62.179.125.152/SK/Images/hrp_6f857ae9375b3bcceb6353a5b35775f52cd85302.jpg"},{"assetType":"boxCover","assetTypes":["boxCover"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg"},{"assetType":"boxart-small","assetTypes":["boxart-small"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=75&h=108&mode=box"},{"assetType":"boxart-medium","assetTypes":["boxart-medium"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=110&h=159&mode=box"},{"assetType":"boxart-xlarge","assetTypes":["boxart-xlarge"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=210&h=303&mode=box"},{"assetType":"boxart-large","assetTypes":["boxart-large"],"url":"http://62.179.125.152/SK/Images/bc_3f5a24412c7f4f434094fa1147a304aa6a5ebda6.jpg?w=180&h=260&mode=box"}],"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fport.cs~~2F71927954","shortDescription":"David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným...","mediaType":"FeatureFilm","year":"2016","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"rootId":"crid:~~2F~~2Fport.cs~~2F71927954","replayTvAvailable":true,"audioTracks":[],"ratings":[],"offersLatestExpirationDate":1676187900000,"replayTvStartOffset":0,"replayTvEndOffset":604800,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false}' + ) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2023-02-06T21:35:00.000Z', + stop: '2023-02-06T23:05:00.000Z', + title: 'Avengement', + description: + 'Během propustky z vězení za účelem návštěvy umírající matky v nemocnici zločinec Cain Burgess (Scott Adkins) unikne svým dozorcům a mizí v ulicích Londýna. Jde o epickou cestu krve a bolesti za dosažením vytoužené pomsty na těch, kteří z něj udělali chladnokrevného vraha.', + category: ['Drama', 'Akcia'], + directors: ['Jesse V. Johnson'], + actors: [ + 'Scott Adkins', + 'Craig Fairbrass', + 'Thomas Turgoose', + 'Nick Moran', + 'Kierston Wareing', + 'Leo Gregory', + 'Mark Strange', + 'Luke LaFontaine', + 'Beau Fowler', + 'Dan Styles', + 'Christopher Sciueref', + 'Matt Routledge', + 'Jane Thorne', + 'Louis Mandylor', + 'Terence Maynard', + 'Greg Burridge', + 'Michael Higgs', + 'Damian Gallagher', + 'Daniel Adegboyega', + 'John Ioannou', + 'Sofie Golding-Spittle', + 'Joe Egan', + 'Darren Swain', + 'Lee Charles', + 'Dominic Kinnaird', + "Ross O'Hennessy", + 'Teresa Mahoney', + 'Andrew Dunkelberger', + 'Sam Hardy', + 'Ivan Moy', + 'Mark Sears', + 'Phillip Ray Tommy' + ], + date: '2019' + }, + { + start: '2023-02-07T04:35:00.000Z', + stop: '2023-02-07T05:00:00.000Z', + title: 'Zoom In', + description: 'Film/Kino', + category: ['Hudba a umenie', 'Film'], + date: '2010' + }, + { + start: '2023-02-07T09:10:00.000Z', + stop: '2023-02-07T11:00:00.000Z', + title: 'Studentka', + description: + 'Ambiciózní vysokoškolačka Valentina (Sophie Marceau) studuje literaturu na pařížské Sorbonně a právě se připravuje k závěrečným zkouškám. Žádný odpočinek, žádné volno, žádné večírky, téměř žádný spánek a především a hlavně ... žádná láska! Věří, že jedině tak obstojí před zkušební komisí. Jednoho dne se však odehraje něco, s čím nepočítala. Potká charismatického hudebníka Neda - a bláznivě se zamiluje. V tuto chvíli stojí před osudovým rozhodnutím: zahodí roky obrovského studijního nasazení, nebo odmítne lásku? Nebo se snad dá obojí skloubit dohromady?', + category: ['Film', 'Komédia'], + actors: [ + 'Sophie Marceauová', + 'Vincent Lindon', + 'Elisabeth Vitali', + 'Elena Pompei', + 'Jean-Claude Leguay', + 'Brigitte Chamarande', + 'Christian Pereira', + 'Gérard Dacier', + 'Roberto Attias', + 'Beppe Chierici', + 'Nathalie Mann', + 'Anne Macina', + 'Janine Souchon', + 'Virginie Demians', + 'Hugues Leforestier', + 'Jacqueline Noëlle', + 'Marc-André Brunet', + 'Isabelle Caubère', + 'André Chazel', + 'Med Salah Cheurfi', + 'Guillaume Corea', + 'Eric Denize', + 'Gilles Gaston-Dreyfuss', + 'Benoît Gourley', + 'Marc Innocenti', + 'Najim Laouriga', + 'Laurent Ledermann', + 'Philippe Maygal', + 'Dominique Pifarely', + 'Ysé Tran' + ], + directors: ['Francis De Gueltz', 'Dominique Talmon', 'Claude Pinoteau'], + date: '1988' + }, + { + start: '2023-02-07T16:05:00.000Z', + stop: '2023-02-07T17:45:00.000Z', + title: 'Zilionáři', + description: + 'David (Zach Galifianakis) je nekomplikovaný muž, který uvízl v monotónním životě. Den co den usedá za volant svého obrněného automobilu, aby odvážel obrovské sumy peněz jiných lidí. Jediným vzrušujícím momentem v jeho životě je flirtování s kolegyní Kelly (Kristen Wiig), která ho však brzy zatáhne do těžko uvěřitelného dobrodružství. Skupinka nepříliš inteligentních loserů, pod vedením Steva (Owen Wilson), plánuje vyloupit banku a David jim v tom má samozřejmě pomoci. Navzdory absolutně amatérskému plánu se ale stane nemožné a oni mají najednou v kapse 17 miliónů dolarů. A protože tato partička je opravdu bláznivá, začne je hned ve velkém roztáčet. Peníze létají vzduchem za luxusní a kolikrát i zbytečné věci, ale nedochází jim, že pro policii tak zanechávají jasné stopy...', + category: ['Drama', 'Akcia'], + actors: [ + 'Zach Galifianakis', + 'Kristen Wiigová', + 'Owen Wilson', + 'Kate McKinnon', + 'Leslie Jones', + 'Jason Sudeikis', + 'Ross Kimball', + 'Devin Ratray', + 'Mary Elizabeth Ellisová', + 'Jon Daly', + 'Ken Marino', + 'Daniel Zacapa', + 'Tom Werme', + 'Njema Williams', + 'Nils Cruz', + 'Michael Fraguada', + 'Christian Gonzalez', + 'Candace Blanchard', + 'Karsten Friske', + 'Dallas Edwards', + 'Barry Ratcliffe', + 'Shelton Grant', + 'Laura Palka', + 'Reegus Flenory', + 'Wynn Reichert', + 'Jill Jane Clements', + 'Joseph S. Wilson', + 'Jee An', + 'Rhoda Griffisová', + 'Nicole Dupre Sobchack' + ], + directors: [ + 'Scott August', + 'Richard L. Fox', + 'Michelle Malley-Campos', + 'Sebastian Mazzola', + 'Steven Ritzi', + 'Pete Waterman', + 'Jared Hess' + ], + date: '2016' + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', + channel, + date + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/i.mjh.nz/i.mjh.nz.config.js b/sites/i.mjh.nz/i.mjh.nz.config.js index 0f81dbf6..1c17a6de 100644 --- a/sites/i.mjh.nz/i.mjh.nz.config.js +++ b/sites/i.mjh.nz/i.mjh.nz.config.js @@ -1,107 +1,107 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const parser = require('epg-parser') -const isBetween = require('dayjs/plugin/isBetween') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(isBetween) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master' - -module.exports = { - site: 'i.mjh.nz', - days: 2, - request: { - cache: { - ttl: 3 * 60 * 60 * 1000 // 3h - }, - maxContentLength: 100 * 1024 * 1024 // 100Mb - }, - url: function ({ channel }) { - const [path] = channel.site_id.split('#') - - return `${API_ENDPOINT}/${path}.xml` - }, - parser: function ({ content, channel, date }) { - const items = parseItems(content, channel, date) - - let programs = items.map(item => { - return { - ...item, - title: getTitle(item), - description: getDescription(item), - categories: getCategories(item) - } - }) - - programs = mergeMovieParts(programs) - - return programs - }, - async channels({ path, lang = 'en' }) { - let xml = await axios - .get(`${API_ENDPOINT}/${path}.xml`) - .then(r => r.data) - .catch(console.log) - let data = parser.parse(xml) - - return data.channels.map(channel => { - return { - lang, - site_id: `${path}#${channel.id}`, - name: channel.name[0].value - } - }) - } -} - -function mergeMovieParts(programs) { - let output = [] - - programs.forEach(prog => { - let prev = output[output.length - 1] - let found = - prev && - prog.categories.includes('Movie') && - prev.title === prog.title && - prev.description === prog.description - - if (found) { - prev.stop = prog.stop - } else { - output.push(prog) - } - }) - - return output -} - -function getTitle(item) { - return item.title.length ? item.title[0].value : null -} - -function getDescription(item) { - return item.desc.length ? item.desc[0].value : null -} - -function getCategories(item) { - return item.category.map(c => c.value) -} - -function parseItems(content, channel, date) { - try { - const curr_day = date - const next_day = date.add(1, 'd') - const [, site_id] = channel.site_id.split('#') - const data = parser.parse(content) - if (!data || !Array.isArray(data.programs)) return [] - - return data.programs.filter( - p => - p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day) - ) - } catch (error) { - return [] - } -} +const dayjs = require('dayjs') +const axios = require('axios') +const parser = require('epg-parser') +const isBetween = require('dayjs/plugin/isBetween') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(isBetween) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master' + +module.exports = { + site: 'i.mjh.nz', + days: 2, + request: { + cache: { + ttl: 3 * 60 * 60 * 1000 // 3h + }, + maxContentLength: 100 * 1024 * 1024 // 100Mb + }, + url: function ({ channel }) { + const [path] = channel.site_id.split('#') + + return `${API_ENDPOINT}/${path}.xml` + }, + parser: function ({ content, channel, date }) { + const items = parseItems(content, channel, date) + + let programs = items.map(item => { + return { + ...item, + title: getTitle(item), + description: getDescription(item), + categories: getCategories(item) + } + }) + + programs = mergeMovieParts(programs) + + return programs + }, + async channels({ path, lang = 'en' }) { + let xml = await axios + .get(`${API_ENDPOINT}/${path}.xml`) + .then(r => r.data) + .catch(console.log) + let data = parser.parse(xml) + + return data.channels.map(channel => { + return { + lang, + site_id: `${path}#${channel.id}`, + name: channel.name[0].value + } + }) + } +} + +function mergeMovieParts(programs) { + let output = [] + + programs.forEach(prog => { + let prev = output[output.length - 1] + let found = + prev && + prog.categories.includes('Movie') && + prev.title === prog.title && + prev.description === prog.description + + if (found) { + prev.stop = prog.stop + } else { + output.push(prog) + } + }) + + return output +} + +function getTitle(item) { + return item.title.length ? item.title[0].value : null +} + +function getDescription(item) { + return item.desc.length ? item.desc[0].value : null +} + +function getCategories(item) { + return item.category.map(c => c.value) +} + +function parseItems(content, channel, date) { + try { + const curr_day = date + const next_day = date.add(1, 'd') + const [, site_id] = channel.site_id.split('#') + const data = parser.parse(content) + if (!data || !Array.isArray(data.programs)) return [] + + return data.programs.filter( + p => + p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day) + ) + } catch (error) { + return [] + } +} diff --git a/sites/i.mjh.nz/i.mjh.nz.test.js b/sites/i.mjh.nz/i.mjh.nz.test.js index c5ba8bc8..8f74c2d7 100644 --- a/sites/i.mjh.nz/i.mjh.nz.test.js +++ b/sites/i.mjh.nz/i.mjh.nz.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/i.mjh.nz/i.mjh.nz.config.js --output=./sites/i.mjh.nz/i.mjh.nz_pluto.channels.xml --set=path:PlutoTV/all -// npm run grab -- --site=i.mjh.nz - -const { parser, url } = require('./i.mjh.nz.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Plex/all#5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', - xmltv_id: 'DarkMatterTV.us', - lang: 'en' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe( - 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master/Plex/all.xml' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) - const results = parser({ content, channel, date }) - - expect(results.length).toBe(11) - expect(results[0]).toMatchObject({ - start: '2023-06-23T07:14:32.000Z', - stop: '2023-06-23T09:09:36.000Z', - title: 'Killers Within', - date: ['20180101'], - description: - 'With her son being held captive by a criminal gang, police officer Amanda Doyle, together with her ex-husband and three unlikely allies, takes part in a desperate plot to hold a wealthy banker and his family to ransom. But this is no ordinary family.', - icon: ['https://provider-static.plex.tv/epg/images/thumbnails/darkmatter-tv-fallback.jpg'], - categories: ['Movie'] - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '404: Not Found', - channel, - date - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/i.mjh.nz/i.mjh.nz.config.js --output=./sites/i.mjh.nz/i.mjh.nz_pluto.channels.xml --set=path:PlutoTV/all +// npm run grab -- --site=i.mjh.nz + +const { parser, url } = require('./i.mjh.nz.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Plex/all#5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', + xmltv_id: 'DarkMatterTV.us', + lang: 'en' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://raw.githubusercontent.com/matthuisman/i.mjh.nz/master/Plex/all.xml' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.xml')) + const results = parser({ content, channel, date }) + + expect(results.length).toBe(11) + expect(results[0]).toMatchObject({ + start: '2023-06-23T07:14:32.000Z', + stop: '2023-06-23T09:09:36.000Z', + title: 'Killers Within', + date: ['20180101'], + description: + 'With her son being held captive by a criminal gang, police officer Amanda Doyle, together with her ex-husband and three unlikely allies, takes part in a desperate plot to hold a wealthy banker and his family to ransom. But this is no ordinary family.', + icon: ['https://provider-static.plex.tv/epg/images/thumbnails/darkmatter-tv-fallback.jpg'], + categories: ['Movie'] + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '404: Not Found', + channel, + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/i24news.tv/i24news.tv.config.js b/sites/i24news.tv/i24news.tv.config.js index 3c590bc5..b67f1f5d 100644 --- a/sites/i24news.tv/i24news.tv.config.js +++ b/sites/i24news.tv/i24news.tv.config.js @@ -1,67 +1,67 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'i24news.tv', - days: 2, - url: function ({ channel }) { - const [lang] = channel.site_id.split('#') - - return `https://api.i24news.tv/v2/${lang}/schedules/world` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - if (!item.show) return - programs.push({ - title: item.show.title, - description: item.show.body, - icon: parseIcon(item), - start: parseStart(item, date), - stop: parseStop(item, date) - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.show.image ? item.show.image.href : null -} - -function parseStart(item, date) { - if (!item.startHour) return null - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.startHour}`, - 'YYYY-MM-DD HH:mm', - 'Asia/Jerusalem' - ) -} - -function parseStop(item, date) { - if (!item.endHour) return null - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.endHour}`, - 'YYYY-MM-DD HH:mm', - 'Asia/Jerusalem' - ) -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - let day = date.day() - 1 - day = day < 0 ? 6 : day - - return data.filter(item => item.day === day) -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'i24news.tv', + days: 2, + url: function ({ channel }) { + const [lang] = channel.site_id.split('#') + + return `https://api.i24news.tv/v2/${lang}/schedules/world` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + if (!item.show) return + programs.push({ + title: item.show.title, + description: item.show.body, + icon: parseIcon(item), + start: parseStart(item, date), + stop: parseStop(item, date) + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.show.image ? item.show.image.href : null +} + +function parseStart(item, date) { + if (!item.startHour) return null + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.startHour}`, + 'YYYY-MM-DD HH:mm', + 'Asia/Jerusalem' + ) +} + +function parseStop(item, date) { + if (!item.endHour) return null + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.endHour}`, + 'YYYY-MM-DD HH:mm', + 'Asia/Jerusalem' + ) +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + let day = date.day() - 1 + day = day < 0 ? 6 : day + + return data.filter(item => item.day === day) +} diff --git a/sites/i24news.tv/i24news.tv.test.js b/sites/i24news.tv/i24news.tv.test.js index ab96ad07..f231d4e5 100644 --- a/sites/i24news.tv/i24news.tv.test.js +++ b/sites/i24news.tv/i24news.tv.test.js @@ -1,46 +1,46 @@ -// npm run grab -- --site=i24news.tv - -const { parser, url } = require('./i24news.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ar#', - xmltv_id: 'I24NewsArabic.il' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules/world') -}) - -it('can parse response', () => { - const content = - '[{"id":348995,"startHour":"22:30","endHour":"23:00","day":5,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}},{"id":349023,"startHour":"15:00","endHour":"15:28","day":6,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}}]' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-06T13:00:00.000Z', - stop: '2022-03-06T13:28:00.000Z', - title: 'تغطية خاصة', - description: 'Special Edition', - icon: 'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=i24news.tv + +const { parser, url } = require('./i24news.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ar#', + xmltv_id: 'I24NewsArabic.il' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://api.i24news.tv/v2/ar/schedules/world') +}) + +it('can parse response', () => { + const content = + '[{"id":348995,"startHour":"22:30","endHour":"23:00","day":5,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}},{"id":349023,"startHour":"15:00","endHour":"15:28","day":6,"firstDiffusion":false,"override":false,"show":{"parsedBody":[{"type":"text","text":"Special Edition"}],"id":131,"title":"تغطية خاصة","body":"Special Edition","slug":"Special-Edition-تغطية-خاصة","visible":true,"image":{"id":1142467,"credit":"","legend":"","href":"https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png"}}}]' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-06T13:00:00.000Z', + stop: '2022-03-06T13:28:00.000Z', + title: 'تغطية خاصة', + description: 'Special Edition', + icon: 'https://cdn.i24news.tv/uploads/a1/be/85/20/69/6f/32/1c/ed/b0/f8/5c/f6/1c/40/f9/a1be8520696f321cedb0f85cf61c40f9.png' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/indihometv.com/indihometv.com.config.js b/sites/indihometv.com/indihometv.com.config.js index 618e909d..57712c2c 100644 --- a/sites/indihometv.com/indihometv.com.config.js +++ b/sites/indihometv.com/indihometv.com.config.js @@ -1,68 +1,68 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'indihometv.com', - days: 2, - url({ channel }) { - return `https://www.indihometv.com/tvod/${channel.site_id}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev && start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('p').text() - const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${start}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseStop($item, date) { - const timeString = $item('p').text() - const [, stop] = timeString.match(/- (\d{2}:\d{2})/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${stop}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseTitle($item) { - return $item('b').text() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - return $(`#pills-${date.format('YYYY-MM-DD')} .schedule-item`).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'indihometv.com', + days: 2, + url({ channel }) { + return `https://www.indihometv.com/tvod/${channel.site_id}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev && start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('p').text() + const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${start}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseStop($item, date) { + const timeString = $item('p').text() + const [, stop] = timeString.match(/- (\d{2}:\d{2})/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${stop}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseTitle($item) { + return $item('b').text() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + return $(`#pills-${date.format('YYYY-MM-DD')} .schedule-item`).toArray() +} diff --git a/sites/indihometv.com/indihometv.com.test.js b/sites/indihometv.com/indihometv.com.test.js index 7d015d36..5f5c5ca0 100644 --- a/sites/indihometv.com/indihometv.com.test.js +++ b/sites/indihometv.com/indihometv.com.test.js @@ -1,58 +1,58 @@ -// npm run grab -- --site=indihometv.com - -const { parser, url } = require('./indihometv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'metrotv', - xmltv_id: 'MetroTV.id' -} -const content = - '
    ' - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.indihometv.com/tvod/metrotv') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Headline News', - start: '2022-08-08T00:00:00.000Z', - stop: '2022-08-08T00:05:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:05:00.000Z', - stop: '2022-08-08T00:30:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:30:00.000Z', - stop: '2022-08-08T00:45:00.000Z' - }, - { - title: 'Editorial Media Indonesia', - start: '2022-08-08T00:45:00.000Z', - stop: '2022-08-08T01:00:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=indihometv.com + +const { parser, url } = require('./indihometv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'metrotv', + xmltv_id: 'MetroTV.id' +} +const content = + '
    ' + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.indihometv.com/tvod/metrotv') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Headline News', + start: '2022-08-08T00:00:00.000Z', + stop: '2022-08-08T00:05:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:05:00.000Z', + stop: '2022-08-08T00:30:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:30:00.000Z', + stop: '2022-08-08T00:45:00.000Z' + }, + { + title: 'Editorial Media Indonesia', + start: '2022-08-08T00:45:00.000Z', + stop: '2022-08-08T01:00:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ionplustv.com/ionplustv.com.config.js b/sites/ionplustv.com/ionplustv.com.config.js index 6dab5fe1..eb370e9e 100644 --- a/sites/ionplustv.com/ionplustv.com.config.js +++ b/sites/ionplustv.com/ionplustv.com.config.js @@ -1,107 +1,107 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'ionplustv.com', - days: 2, - url({ date }) { - return `https://ionplustv.com/schedule/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const duration = parseDuration($item) - let stop = start.add(duration, 'm') - - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - rating: parseRating($item), - start, - stop - }) - } - - return programs - } -} - -function parseDescription($item) { - return $item('.panel-body > div > div > div > p:nth-child(2)').text().trim() -} - -function parseIcon($item) { - return $item('.video-thumbnail img').attr('src') -} - -function parseTitle($item) { - return $item('.show-title').text().trim() -} - -function parseSubTitle($item) { - return $item('.panel-title > div > div > div > div:nth-child(2) > p') - .text() - .trim() - .replace(/\s\s+/g, ' ') -} - -function parseRating($item) { - const [, rating] = $item('.tv-rating') - .text() - .match(/([^(]+)/) || [null, null] - - return rating - ? { - system: 'MPA', - value: rating.trim() - } - : null -} - -function parseStart($item, date) { - let time = $item('.panel-title h2').clone().children().remove().end().text().trim() - time = time.includes(':') ? time : time + ':00' - const meridiem = $item('.panel-title h2 > .meridiem').text().trim() - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${time} ${meridiem}`, - 'YYYY-MM-DD H:mm A', - 'America/New_York' - ) -} - -function parseDuration($item) { - const [, duration] = $item('.tv-rating') - .text() - .trim() - .match(/\((\d+)/) || [null, null] - - return parseInt(duration) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('#accordion > div').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ionplustv.com', + days: 2, + url({ date }) { + return `https://ionplustv.com/schedule/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const duration = parseDuration($item) + let stop = start.add(duration, 'm') + + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + rating: parseRating($item), + start, + stop + }) + } + + return programs + } +} + +function parseDescription($item) { + return $item('.panel-body > div > div > div > p:nth-child(2)').text().trim() +} + +function parseIcon($item) { + return $item('.video-thumbnail img').attr('src') +} + +function parseTitle($item) { + return $item('.show-title').text().trim() +} + +function parseSubTitle($item) { + return $item('.panel-title > div > div > div > div:nth-child(2) > p') + .text() + .trim() + .replace(/\s\s+/g, ' ') +} + +function parseRating($item) { + const [, rating] = $item('.tv-rating') + .text() + .match(/([^(]+)/) || [null, null] + + return rating + ? { + system: 'MPA', + value: rating.trim() + } + : null +} + +function parseStart($item, date) { + let time = $item('.panel-title h2').clone().children().remove().end().text().trim() + time = time.includes(':') ? time : time + ':00' + const meridiem = $item('.panel-title h2 > .meridiem').text().trim() + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${time} ${meridiem}`, + 'YYYY-MM-DD H:mm A', + 'America/New_York' + ) +} + +function parseDuration($item) { + const [, duration] = $item('.tv-rating') + .text() + .trim() + .match(/\((\d+)/) || [null, null] + + return parseInt(duration) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('#accordion > div').toArray() +} diff --git a/sites/ionplustv.com/ionplustv.com.test.js b/sites/ionplustv.com/ionplustv.com.test.js index 076ca9f6..460bb72f 100644 --- a/sites/ionplustv.com/ionplustv.com.test.js +++ b/sites/ionplustv.com/ionplustv.com.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=ionplustv.com - -const { parser, url } = require('./ionplustv.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://ionplustv.com/schedule/2022-11-08') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-08T10:00:00.000Z', - stop: '2022-11-08T11:00:00.000Z', - title: 'All For Nothing?', - sub_title: '226 : Randy & Sarita Vs. Jean-marcel & Melodie', - icon: 'https://ionplustv.com/static/programs/shows/all-for-nothing/show-banner-all-for-nothing-5ab162f2d8ee6-897aca6d7d9a7d4e2026ca3b592d8b2a047238fa.png', - rating: { - system: 'MPA', - value: 'TV-PG+L' - }, - description: - "Randy and Sarita want to take their relationship to the next level and move-in together. Blending their families will require space for seven so they must sell Randy's dated bungalow for top dollar. Paul and Penny have differing opinions on the best plan for this house, but they do agree that all the wallpaper boarders must go! Having struggled to get the demolition started, Randy and Sarita turn up the reno pace in the second week which includes gambling on a poker night fundraiser. In preparation for retirement, Jean-Marcel and Melodie are ready to downsize. Having been out of the real estate market for ages, they have no idea how to ˜wow' the buyers of today. Armed with Paul and Penny's job list to bring their house into the now, they make major progress on day one. Flu, leaks, and a free shower insert that won't fit into their bathroom slow down their pace giving the competition a chance to overtake their early lead." - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), - date - }) - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=ionplustv.com + +const { parser, url } = require('./ionplustv.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-08', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://ionplustv.com/schedule/2022-11-08') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-08T10:00:00.000Z', + stop: '2022-11-08T11:00:00.000Z', + title: 'All For Nothing?', + sub_title: '226 : Randy & Sarita Vs. Jean-marcel & Melodie', + icon: 'https://ionplustv.com/static/programs/shows/all-for-nothing/show-banner-all-for-nothing-5ab162f2d8ee6-897aca6d7d9a7d4e2026ca3b592d8b2a047238fa.png', + rating: { + system: 'MPA', + value: 'TV-PG+L' + }, + description: + "Randy and Sarita want to take their relationship to the next level and move-in together. Blending their families will require space for seven so they must sell Randy's dated bungalow for top dollar. Paul and Penny have differing opinions on the best plan for this house, but they do agree that all the wallpaper boarders must go! Having struggled to get the demolition started, Randy and Sarita turn up the reno pace in the second week which includes gambling on a poker night fundraiser. In preparation for retirement, Jean-Marcel and Melodie are ready to downsize. Having been out of the real estate market for ages, they have no idea how to ˜wow' the buyers of today. Armed with Paul and Penny's job list to bring their house into the now, they make major progress on day one. Flu, leaks, and a free shower insert that won't fit into their bathroom slow down their pace giving the competition a chance to overtake their early lead." + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), + date + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/ipko.com/ipko.com.config.js b/sites/ipko.com/ipko.com.config.js index b2f2dfd3..9d30ff08 100644 --- a/sites/ipko.com/ipko.com.config.js +++ b/sites/ipko.com/ipko.com.config.js @@ -1,46 +1,46 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'ipko.com', - days: 2, - url: function ({ date }) { - return `https://www.ipko.com/epg/admin/programs.php?date=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.duration / 3, 'm') - - programs.push({ - title: item.program_name, - description: item.description, - category: item.category, - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.date}` - - return dayjs.utc(time, 'YYYY-MM-DD HH:mm:ss') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - const arr = Object.values(data.element) - const items = arr.find(el => { - return el[0] && el[0].channel_id == channel.site_id - }) - - return Array.isArray(items) ? items : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'ipko.com', + days: 2, + url: function ({ date }) { + return `https://www.ipko.com/epg/admin/programs.php?date=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.duration / 3, 'm') + + programs.push({ + title: item.program_name, + description: item.description, + category: item.category, + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.date}` + + return dayjs.utc(time, 'YYYY-MM-DD HH:mm:ss') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + const arr = Object.values(data.element) + const items = arr.find(el => { + return el[0] && el[0].channel_id == channel.site_id + }) + + return Array.isArray(items) ? items : [] +} diff --git a/sites/ipko.com/ipko.com.test.js b/sites/ipko.com/ipko.com.test.js index dc0517a8..298175f5 100644 --- a/sites/ipko.com/ipko.com.test.js +++ b/sites/ipko.com/ipko.com.test.js @@ -1,40 +1,40 @@ -// npm run grab -- --site=ipko.com - -const { parser, url } = require('./ipko.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '406', - xmltv_id: 'RTK1.xk' -} -const content = - '{"element":{"1":[{"id":6367,"channel_id":406,"program_name":"Beautiful People 13","name_short":"","description":"Lin largohet nga Nju Meksiko për t\'u vendosur në Nju Jork e për t\'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë","category":"Sezoni I","duration":150,"day":"Sun","left_distanc":165,"date":"00:55:00"}]}}' - -it('can generate valid url', () => { - const result = url({ date }) - expect(result).toBe('https://www.ipko.com/epg/admin/programs.php?date=2021-10-24') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: 'Sun, 24 Oct 2021 00:55:00 GMT', - stop: 'Sun, 24 Oct 2021 01:45:00 GMT', - title: 'Beautiful People 13', - description: - "Lin largohet nga Nju Meksiko për t'u vendosur në Nju Jork e për t'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë", - category: 'Sezoni I' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{"element":{"1":[{"no":"no"}]}}' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=ipko.com + +const { parser, url } = require('./ipko.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '406', + xmltv_id: 'RTK1.xk' +} +const content = + '{"element":{"1":[{"id":6367,"channel_id":406,"program_name":"Beautiful People 13","name_short":"","description":"Lin largohet nga Nju Meksiko për t\'u vendosur në Nju Jork e për t\'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë","category":"Sezoni I","duration":150,"day":"Sun","left_distanc":165,"date":"00:55:00"}]}}' + +it('can generate valid url', () => { + const result = url({ date }) + expect(result).toBe('https://www.ipko.com/epg/admin/programs.php?date=2021-10-24') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: 'Sun, 24 Oct 2021 00:55:00 GMT', + stop: 'Sun, 24 Oct 2021 01:45:00 GMT', + title: 'Beautiful People 13', + description: + "Lin largohet nga Nju Meksiko për t'u vendosur në Nju Jork e për t'ia nisur nga fillimi: një punë të re, shtëpi të re dhe njohje të reja. Bashkë me të janë vajzat e saj, Sofia, një 16 vjeçare që shkëlqen në shkollë, dhe Kareni, 20 vjeçare, që do të bë", + category: 'Sezoni I' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{"element":{"1":[{"no":"no"}]}}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/kan.org.il/kan.org.il.config.js b/sites/kan.org.il/kan.org.il.config.js index 987b4aa3..0170d550 100644 --- a/sites/kan.org.il/kan.org.il.config.js +++ b/sites/kan.org.il/kan.org.il.config.js @@ -1,52 +1,52 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'kan.org.il', - days: 2, - url: function ({ channel, date }) { - return `https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=${ - channel.site_id - }&day=${date.format('DD/MM/YYYY')}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.live_desc, - icon: item.picture_code, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.start_time) return null - - return dayjs.tz(item.start_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') -} - -function parseStop(item) { - if (!item.end_time) return null - - return dayjs.tz(item.end_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - - return data -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'kan.org.il', + days: 2, + url: function ({ channel, date }) { + return `https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=${ + channel.site_id + }&day=${date.format('DD/MM/YYYY')}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.live_desc, + icon: item.picture_code, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.start_time) return null + + return dayjs.tz(item.start_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') +} + +function parseStop(item) { + if (!item.end_time) return null + + return dayjs.tz(item.end_time, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Jerusalem') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + + return data +} diff --git a/sites/kan.org.il/kan.org.il.test.js b/sites/kan.org.il/kan.org.il.test.js index c2aa9710..616265b5 100644 --- a/sites/kan.org.il/kan.org.il.test.js +++ b/sites/kan.org.il/kan.org.il.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=kan.org.il - -const { parser, url } = require('./kan.org.il.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '19', - xmltv_id: 'KANEducational.il' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022' - ) -}) - -it('can parse response', () => { - const content = - '[{"title":"ארץ מולדת - בין תורכיה לבריטניה","start_time":"2022-03-06T00:05:37","end_time":"2022-03-06T00:27:12","id":"2598","age_category_desc":"0","epg_name":"ארץ מולדת","title1":"ארץ מולדת - בין תורכיה לבריטניה","chapter_number":"9","live_desc":"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות\'מאני לבין תקוותיו מהבריטים הכובשים.","Station_Radio":"0","Station_Id":"20","stationUrlScheme":"kan11://plugin/?type=player&plugin_identifier=kan_player&ds=general-provider%3A%2F%2FfetchData%3Ftype%3DFEED_JSON%26url%3DaHR0cHM6Ly93d3cua2FuLm9yZy5pbC9hcHBLYW4vbGl2ZVN0YXRpb25zLmFzaHg%3D&id=4","program_code":"3671","picture_code":"https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg","program_image":"","station_image":"Logo_Image_Logo20_img__8.jpg","program_id":"","timezone":"2"}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-05T22:05:37.000Z', - stop: '2022-03-05T22:27:12.000Z', - title: 'ארץ מולדת - בין תורכיה לבריטניה', - description: - "קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.", - icon: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=kan.org.il + +const { parser, url } = require('./kan.org.il.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '19', + xmltv_id: 'KANEducational.il' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.kan.org.il/tv-guide/tv_guidePrograms.ashx?stationID=19&day=06/03/2022' + ) +}) + +it('can parse response', () => { + const content = + '[{"title":"ארץ מולדת - בין תורכיה לבריטניה","start_time":"2022-03-06T00:05:37","end_time":"2022-03-06T00:27:12","id":"2598","age_category_desc":"0","epg_name":"ארץ מולדת","title1":"ארץ מולדת - בין תורכיה לבריטניה","chapter_number":"9","live_desc":"קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות\'מאני לבין תקוותיו מהבריטים הכובשים.","Station_Radio":"0","Station_Id":"20","stationUrlScheme":"kan11://plugin/?type=player&plugin_identifier=kan_player&ds=general-provider%3A%2F%2FfetchData%3Ftype%3DFEED_JSON%26url%3DaHR0cHM6Ly93d3cua2FuLm9yZy5pbC9hcHBLYW4vbGl2ZVN0YXRpb25zLmFzaHg%3D&id=4","program_code":"3671","picture_code":"https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg","program_image":"","station_image":"Logo_Image_Logo20_img__8.jpg","program_id":"","timezone":"2"}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-05T22:05:37.000Z', + stop: '2022-03-05T22:27:12.000Z', + title: 'ארץ מולדת - בין תורכיה לבריטניה', + description: + "קבוצת תלמידים מתארגנת בפרוץ מלחמת העולם הראשונה להגיש עזרה לישוב. באמצעות התלמידים לומד הצופה על בעיותיו של הישוב בתקופת המלחמה, והתלבטותו בין נאמנות לשלטון העות'מאני לבין תקוותיו מהבריטים הכובשים.", + icon: 'https://kanweb.blob.core.windows.net/download/pictures/2021/1/20/imgid=45847_Z.jpeg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/knr.gl/knr.gl.config.js b/sites/knr.gl/knr.gl.config.js index c9ca8fe2..fc5492f8 100644 --- a/sites/knr.gl/knr.gl.config.js +++ b/sites/knr.gl/knr.gl.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'knr.gl', - days: 2, - url({ date }) { - return `https://knr.gl/admin/knr/TV/program/${date.format('YYYY-MM-DD')}/gl` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const start = parseStart(item, date) - const stop = start.add(1, 'h') - if (prev) prev.stop = start - programs.push({ - title: item.title, - description: item.description, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'America/Godthab') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data.program_list) return [] - const $ = cheerio.load(data.program_list) - const items = [] - $('dt').each(function () { - const titleElem = $(this) - .contents() - .filter(function () { - return this.nodeType === 3 - })[0] - items.push({ - title: titleElem.nodeValue.trim(), - description: $(this) - .next('dd') - .text() - .replace(/(\r\n|\n|\r)/gm, ' '), - time: $(this, 'strong').text().trim() - }) - }) - - return items -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'knr.gl', + days: 2, + url({ date }) { + return `https://knr.gl/admin/knr/TV/program/${date.format('YYYY-MM-DD')}/gl` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const start = parseStart(item, date) + const stop = start.add(1, 'h') + if (prev) prev.stop = start + programs.push({ + title: item.title, + description: item.description, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'America/Godthab') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data.program_list) return [] + const $ = cheerio.load(data.program_list) + const items = [] + $('dt').each(function () { + const titleElem = $(this) + .contents() + .filter(function () { + return this.nodeType === 3 + })[0] + items.push({ + title: titleElem.nodeValue.trim(), + description: $(this) + .next('dd') + .text() + .replace(/(\r\n|\n|\r)/gm, ' '), + time: $(this, 'strong').text().trim() + }) + }) + + return items +} diff --git a/sites/knr.gl/knr.gl.test.js b/sites/knr.gl/knr.gl.test.js index a0cb8cd4..8c0a50b7 100644 --- a/sites/knr.gl/knr.gl.test.js +++ b/sites/knr.gl/knr.gl.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=knr.gl - -const { parser, url } = require('./knr.gl.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KNRTV.gl' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://knr.gl/admin/knr/TV/program/2021-11-22/gl') -}) - -it('can parse response', () => { - const content = - '{"program_list":"\\u003Cdt class=\\u0022program\\u0022\\u003E\\u003Cstrong\\u003E08:00\\u003C\\/strong\\u003E Meeqqanut - Toqqorsivimmiit\\u003C\\/dt\\u003E\\u003Cdt class=\\u0022knr-program-pointer knr-program-togle-program\\u0022 data-program-id=\\u0022588574\\u0022 data-module-path=\\u0022sites\\/knr\\/modules\\/custom\\/knr_site\\u0022\\u003E\\u003Cimg height=\\u00229\\u0022 width=\\u00229\\u0022 id=\\u0022icon_588574\\u0022 alt=\\u0022View description\\u0022 src=\\u0022\\/sites\\/knr\\/modules\\/custom\\/knr_site\\/assets\\/img\\/plus.gif\\u0022\\u003E\\u003Cstrong\\u003E08:30\\u003C\\/strong\\u003E ICC 2018 Piorsarsimassutikkut pisut (1:3)\\u003C\\/dt\\u003E\\u003Cdd id=\\u0022program_588574\\u0022 style=\\u0022display: none;\\u0022\\u003E\\u003Cdiv class=\\u0022box\\u0022\\u003E2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen\\r\\nKNR 09.12.2018\\u003C\\/div\\u003E\\u003C\\/dd\\u003E"}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-22T11:00:00.000Z', - stop: '2021-11-22T11:30:00.000Z', - title: 'Meeqqanut - Toqqorsivimmiit' - }, - { - start: '2021-11-22T11:30:00.000Z', - stop: '2021-11-22T12:30:00.000Z', - title: 'ICC 2018 Piorsarsimassutikkut pisut (1:3)', - description: - '2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen KNR 09.12.2018' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"program_list":""}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=knr.gl + +const { parser, url } = require('./knr.gl.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KNRTV.gl' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://knr.gl/admin/knr/TV/program/2021-11-22/gl') +}) + +it('can parse response', () => { + const content = + '{"program_list":"\\u003Cdt class=\\u0022program\\u0022\\u003E\\u003Cstrong\\u003E08:00\\u003C\\/strong\\u003E Meeqqanut - Toqqorsivimmiit\\u003C\\/dt\\u003E\\u003Cdt class=\\u0022knr-program-pointer knr-program-togle-program\\u0022 data-program-id=\\u0022588574\\u0022 data-module-path=\\u0022sites\\/knr\\/modules\\/custom\\/knr_site\\u0022\\u003E\\u003Cimg height=\\u00229\\u0022 width=\\u00229\\u0022 id=\\u0022icon_588574\\u0022 alt=\\u0022View description\\u0022 src=\\u0022\\/sites\\/knr\\/modules\\/custom\\/knr_site\\/assets\\/img\\/plus.gif\\u0022\\u003E\\u003Cstrong\\u003E08:30\\u003C\\/strong\\u003E ICC 2018 Piorsarsimassutikkut pisut (1:3)\\u003C\\/dt\\u003E\\u003Cdd id=\\u0022program_588574\\u0022 style=\\u0022display: none;\\u0022\\u003E\\u003Cdiv class=\\u0022box\\u0022\\u003E2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen\\r\\nKNR 09.12.2018\\u003C\\/div\\u003E\\u003C\\/dd\\u003E"}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-22T11:00:00.000Z', + stop: '2021-11-22T11:30:00.000Z', + title: 'Meeqqanut - Toqqorsivimmiit' + }, + { + start: '2021-11-22T11:30:00.000Z', + stop: '2021-11-22T12:30:00.000Z', + title: 'ICC 2018 Piorsarsimassutikkut pisut (1:3)', + description: + '2018 ICC ataatsimersuareernerata kingorna unnukkut piorsarsimassutsikkut pisut takutinneqarput. Aammalu illoqarfik Utqiagvik ilisaritinneqarluni. Ove Heilmann, Aannguaq Nielsen, Aannguaq Reimer-Johansen KNR 09.12.2018' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"program_list":""}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/kplus.vn/kplus.vn.config.js b/sites/kplus.vn/kplus.vn.config.js index a134f1cd..9ed52fd7 100644 --- a/sites/kplus.vn/kplus.vn.config.js +++ b/sites/kplus.vn/kplus.vn.config.js @@ -1,79 +1,79 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) -dayjs.extend(utc) - -const API_ENDPOINT = 'https://www.kplus.vn/Schedule/getSchedule' - -module.exports = { - site: 'kplus.vn', - days: 2, - url: API_ENDPOINT, - request: { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data({ date }) { - const params = new URLSearchParams() - params.append('date', date.format('D-M-YYYY')) - params.append('categories', '') - - return params - }, - method: 'POST' - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - const start = parseStart(item) - const stop = start.add(1, 'h') - if (prev) prev.stop = start - programs.push({ - title: item.Program.Name, - icon: item.Program.Images, - category: item.Program.Genres, - start, - stop - }) - }) - - return programs - }, - async channels() { - const params = new URLSearchParams() - params.append('date', dayjs().format('D-M-YYYY')) - params.append('categories', '') - const data = await axios - .post(API_ENDPOINT, params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }) - .then(r => r.data) - .catch(console.log) - - return data.Channels.map(item => { - return { - lang: 'vi', - site_id: item.Id, - name: item.Name - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.ShowingTime, 'Asia/Ho_Chi_Minh') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.Schedules)) return [] - - return data.Schedules.filter(i => i.ChannelId == channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) +dayjs.extend(utc) + +const API_ENDPOINT = 'https://www.kplus.vn/Schedule/getSchedule' + +module.exports = { + site: 'kplus.vn', + days: 2, + url: API_ENDPOINT, + request: { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data({ date }) { + const params = new URLSearchParams() + params.append('date', date.format('D-M-YYYY')) + params.append('categories', '') + + return params + }, + method: 'POST' + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + const start = parseStart(item) + const stop = start.add(1, 'h') + if (prev) prev.stop = start + programs.push({ + title: item.Program.Name, + icon: item.Program.Images, + category: item.Program.Genres, + start, + stop + }) + }) + + return programs + }, + async channels() { + const params = new URLSearchParams() + params.append('date', dayjs().format('D-M-YYYY')) + params.append('categories', '') + const data = await axios + .post(API_ENDPOINT, params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + .then(r => r.data) + .catch(console.log) + + return data.Channels.map(item => { + return { + lang: 'vi', + site_id: item.Id, + name: item.Name + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.ShowingTime, 'Asia/Ho_Chi_Minh') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.Schedules)) return [] + + return data.Schedules.filter(i => i.ChannelId == channel.site_id) +} diff --git a/sites/kplus.vn/kplus.vn.test.js b/sites/kplus.vn/kplus.vn.test.js index ddadd447..e6a359f4 100644 --- a/sites/kplus.vn/kplus.vn.test.js +++ b/sites/kplus.vn/kplus.vn.test.js @@ -1,67 +1,67 @@ -// npm run channels:parse -- --config=sites/kplus.vn/kplus.vn.config.js --output=sites/kplus.vn/kplus.vn.channels.xml -// npm run grab -- --site=kplus.vn - -const { parser, url, request } = require('./kplus.vn.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '7019', - xmltv_id: 'KPlus1HD.vn' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.kplus.vn/Schedule/getSchedule') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ date }) - - expect(data.get('date')).toBe('15-3-2022') - expect(data.get('categories')).toBe('') -}) - -it('can parse response', () => { - const content = - '{"SchedulesCount":1105,"ChannelsCount":28,"Schedules":[{"Id":12195,"ChannelId":7019,"ProgramId":35111026,"EpgProgramId":"1252496\\r","ShowingTime":"2022-03-15T06:15:00","EpgBroadcastId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","EpgId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T06:22:45","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111026,"Name":"WEEKLY FILMS AND STARS, EP740","BodyContent":"","Cast":"","Director":"","Duration":0,"EpgId":"93701","EpgProgramId":null,"Episode":0,"Genres":"Documentary","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93701/weekly-films-and-stars-ep740","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null},{"Id":12196,"ChannelId":7019,"ProgramId":35111279,"EpgProgramId":"798685\\r","ShowingTime":"2022-03-15T07:00:00","EpgBroadcastId":"HD_MOV_COM__2632318_4383386_OnAir","EpgId":"HD_MOV_COM__2632318_4383386_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T07:02:46","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111279,"Name":"ST. VINCENT","BodyContent":"","Cast":"Bill Murray, Melissa McCarthy, Naomi Watts","Director":"Theodore Melfi","Duration":0,"EpgId":"93959","EpgProgramId":null,"Episode":0,"Genres":"Comedy","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93959/st-vincent","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-14T23:15:00.000Z', - stop: '2022-03-15T00:00:00.000Z', - title: 'WEEKLY FILMS AND STARS, EP740', - icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg', - category: 'Documentary' - }, - { - start: '2022-03-15T00:00:00.000Z', - stop: '2022-03-15T01:00:00.000Z', - title: 'ST. VINCENT', - icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg', - category: 'Comedy' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"SchedulesCount":0,"ChannelsCount":0,"Schedules":[],"Channels":[],"MinDuration":0}', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/kplus.vn/kplus.vn.config.js --output=sites/kplus.vn/kplus.vn.channels.xml +// npm run grab -- --site=kplus.vn + +const { parser, url, request } = require('./kplus.vn.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '7019', + xmltv_id: 'KPlus1HD.vn' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.kplus.vn/Schedule/getSchedule') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ date }) + + expect(data.get('date')).toBe('15-3-2022') + expect(data.get('categories')).toBe('') +}) + +it('can parse response', () => { + const content = + '{"SchedulesCount":1105,"ChannelsCount":28,"Schedules":[{"Id":12195,"ChannelId":7019,"ProgramId":35111026,"EpgProgramId":"1252496\\r","ShowingTime":"2022-03-15T06:15:00","EpgBroadcastId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","EpgId":"HD_ENT_DOC_LNO_21_2649421_2652183_4383385_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T06:22:45","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111026,"Name":"WEEKLY FILMS AND STARS, EP740","BodyContent":"","Cast":"","Director":"","Duration":0,"EpgId":"93701","EpgProgramId":null,"Episode":0,"Genres":"Documentary","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93701/weekly-films-and-stars-ep740","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null},{"Id":12196,"ChannelId":7019,"ProgramId":35111279,"EpgProgramId":"798685\\r","ShowingTime":"2022-03-15T07:00:00","EpgBroadcastId":"HD_MOV_COM__2632318_4383386_OnAir","EpgId":"HD_MOV_COM__2632318_4383386_OnAir","IsDeleted":false,"CreatedOn":"2022-03-15T07:02:46","UpdatedOn":"0001-01-01T00:00:00","Channel":{"Id":7019,"Name":"K+1 HD","Image":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","LiveUrlSegment":"highlights/broadcast-schedule/K-1-HD","FeatureImage":"https://kplus-website-production-cdn.azureedge.net/content/upload/7/images-mkt/logo-k-1-hd-new.png","EpgId":null,"IsOTTEnabled":false,"StartOver":0,"DisplayOrder":0},"Program":{"Id":35111279,"Name":"ST. VINCENT","BodyContent":"","Cast":"Bill Murray, Melissa McCarthy, Naomi Watts","Director":"Theodore Melfi","Duration":0,"EpgId":"93959","EpgProgramId":null,"Episode":0,"Genres":"Comedy","Images":"https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg","IsFeatured":false,"IsOTTEnabled":true,"IsRebroadcast":false,"ShortDescription":"","SubTitle":"","Trailers":"","UrlSegment":"highlights/broadcast-schedule/93959/st-vincent","CreatedOn":"2022-03-16T00:15:45","UpdatedOn":"2022-03-16T00:15:45","ParentalRating":null},"RelatedSchedules":null}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-14T23:15:00.000Z', + stop: '2022-03-15T00:00:00.000Z', + title: 'WEEKLY FILMS AND STARS, EP740', + icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2022_02/ENT_DOC_LNO_21_2649421_2652183_2652183.jpg', + category: 'Documentary' + }, + { + start: '2022-03-15T00:00:00.000Z', + stop: '2022-03-15T01:00:00.000Z', + title: 'ST. VINCENT', + icon: 'https://img.kplus.vn/images?filename=Media/HDVN/2020_05/MOV_COM__2632318_2632318.jpg', + category: 'Comedy' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"SchedulesCount":0,"ChannelsCount":0,"Schedules":[],"Channels":[],"MinDuration":0}', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/kvf.fo/kvf.fo.config.js b/sites/kvf.fo/kvf.fo.config.js index f95a27b9..f5381412 100644 --- a/sites/kvf.fo/kvf.fo.config.js +++ b/sites/kvf.fo/kvf.fo.config.js @@ -1,71 +1,71 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'kvf.fo', - days: 2, - url({ date }) { - return `https://kvf.fo/nskra/sv?date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev && start.isBefore(prev.stop)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const string = $item('.s-normal > .s-time1').text().trim() - let [time] = string.match(/^(\d{2}:\d{2})/g) || [null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') -} - -function parseStop($item, date) { - const string = $item('.s-normal > .s-time1').text().trim() - let [time] = string.match(/(\d{2}:\d{2})$/g) || [null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') -} - -function parseTitle($item) { - return $item('.s-normal > .s-heiti').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.view > .view-content > div.views-row').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'kvf.fo', + days: 2, + url({ date }) { + return `https://kvf.fo/nskra/sv?date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev && start.isBefore(prev.stop)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const string = $item('.s-normal > .s-time1').text().trim() + let [time] = string.match(/^(\d{2}:\d{2})/g) || [null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') +} + +function parseStop($item, date) { + const string = $item('.s-normal > .s-time1').text().trim() + let [time] = string.match(/(\d{2}:\d{2})$/g) || [null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Atlantic/Faroe') +} + +function parseTitle($item) { + return $item('.s-normal > .s-heiti').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.view > .view-content > div.views-row').toArray() +} diff --git a/sites/kvf.fo/kvf.fo.test.js b/sites/kvf.fo/kvf.fo.test.js index 637ea272..bf61455f 100644 --- a/sites/kvf.fo/kvf.fo.test.js +++ b/sites/kvf.fo/kvf.fo.test.js @@ -1,44 +1,44 @@ -// npm run grab -- --site=kvf.fo - -const { parser, url } = require('./kvf.fo.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KVFSjonvarp.fo' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://kvf.fo/nskra/sv?date=2021-11-21') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, './__data__/example.html')) - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result[2]).toMatchObject({ - start: '2021-11-21T18:05:00.000Z', - stop: '2021-11-21T18:30:00.000Z', - title: 'Letibygd 13' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=kvf.fo + +const { parser, url } = require('./kvf.fo.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KVFSjonvarp.fo' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://kvf.fo/nskra/sv?date=2021-11-21') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, './__data__/example.html')) + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result[2]).toMatchObject({ + start: '2021-11-21T18:05:00.000Z', + stop: '2021-11-21T18:30:00.000Z', + title: 'Letibygd 13' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/m.tv.sms.cz/m.tv.sms.cz.config.js b/sites/m.tv.sms.cz/m.tv.sms.cz.config.js index fadfd686..de1c4d98 100644 --- a/sites/m.tv.sms.cz/m.tv.sms.cz.config.js +++ b/sites/m.tv.sms.cz/m.tv.sms.cz.config.js @@ -1,60 +1,60 @@ -const cheerio = require('cheerio') -const iconv = require('iconv-lite') -const { DateTime } = require('luxon') - -module.exports = { - site: 'm.tv.sms.cz', - days: 2, - url: function ({ date, channel }) { - return `https://m.tv.sms.cz/index.php?stanice=${channel.site_id}&cas=0&den=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ buffer, date }) { - const programs = [] - const items = parseItems(buffer) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('div > span').text().trim() - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Prague' }).toUTC() -} - -function parseDescription($item) { - return $item('a.nazev > div.detail').text().trim() -} - -function parseTitle($item) { - return $item('a.nazev > div:nth-child(1)').text().trim() -} - -function parseItems(buffer) { - const string = iconv.decode(buffer, 'win1250') - const $ = cheerio.load(string) - - return $('#obsah > div > div.porady > div.porad').toArray() -} +const cheerio = require('cheerio') +const iconv = require('iconv-lite') +const { DateTime } = require('luxon') + +module.exports = { + site: 'm.tv.sms.cz', + days: 2, + url: function ({ date, channel }) { + return `https://m.tv.sms.cz/index.php?stanice=${channel.site_id}&cas=0&den=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ buffer, date }) { + const programs = [] + const items = parseItems(buffer) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('div > span').text().trim() + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Prague' }).toUTC() +} + +function parseDescription($item) { + return $item('a.nazev > div.detail').text().trim() +} + +function parseTitle($item) { + return $item('a.nazev > div:nth-child(1)').text().trim() +} + +function parseItems(buffer) { + const string = iconv.decode(buffer, 'win1250') + const $ = cheerio.load(string) + + return $('#obsah > div > div.porady > div.porad').toArray() +} diff --git a/sites/m.tv.sms.cz/m.tv.sms.cz.test.js b/sites/m.tv.sms.cz/m.tv.sms.cz.test.js index 9346a5af..6ce326a3 100644 --- a/sites/m.tv.sms.cz/m.tv.sms.cz.test.js +++ b/sites/m.tv.sms.cz/m.tv.sms.cz.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=m.tv.sms.cz - -const { parser, url } = require('./m.tv.sms.cz.config.js') -const iconv = require('iconv-lite') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Cero', - xmltv_id: '0.es' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://m.tv.sms.cz/index.php?stanice=Cero&cas=0&den=2023-06-11' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const buffer = iconv.encode(content, 'win1250') - const results = parser({ buffer, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-11T03:21:00.000Z', - stop: '2023-06-11T04:08:00.000Z', - title: 'Conspiraciones al descubierto: La bomba atómica alemana y el hundimiento del Titanic', - description: 'Documentales' - }) - - expect(results[25]).toMatchObject({ - start: '2023-06-12T02:23:00.000Z', - stop: '2023-06-12T03:23:00.000Z', - title: 'Rapa I (6)', - description: 'Series' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - buffer: iconv.encode( - Buffer.from( - '' - ), - 'win1250' - ) - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=m.tv.sms.cz + +const { parser, url } = require('./m.tv.sms.cz.config.js') +const iconv = require('iconv-lite') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Cero', + xmltv_id: '0.es' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://m.tv.sms.cz/index.php?stanice=Cero&cas=0&den=2023-06-11' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const buffer = iconv.encode(content, 'win1250') + const results = parser({ buffer, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-11T03:21:00.000Z', + stop: '2023-06-11T04:08:00.000Z', + title: 'Conspiraciones al descubierto: La bomba atómica alemana y el hundimiento del Titanic', + description: 'Documentales' + }) + + expect(results[25]).toMatchObject({ + start: '2023-06-12T02:23:00.000Z', + stop: '2023-06-12T03:23:00.000Z', + title: 'Rapa I (6)', + description: 'Series' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + buffer: iconv.encode( + Buffer.from( + '' + ), + 'win1250' + ) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/magentatv.at/magentatv.at.config.js b/sites/magentatv.at/magentatv.at.config.js index 71f16b0e..50235196 100644 --- a/sites/magentatv.at/magentatv.at.config.js +++ b/sites/magentatv.at/magentatv.at.config.js @@ -1,130 +1,130 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' -const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' - -module.exports = { - site: 'magentatv.at', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `${API_STATIC_ENDPOINT}/de/events/segments/${date.format('YYYYMMDDHHmmss')}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const promises = [ - axios.get( - `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(6, 'h').format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(12, 'h').format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(18, 'h').format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ) - ] - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.title, - sub_title: detail.episodeName, - description: detail.longDescription, - date: detail.productionDate, - category: detail.genres, - actors: detail.actors, - directors: detail.directors, - producers: detail.producers, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=de&productClass=Orion-DASH`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'de', - site_id: item.id, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.id) return {} - const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.entries)) return [] - const channelData = data.entries.find(e => e.channelId === channel.site_id) - if (!channelData) return [] - - return Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber -} - -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' +const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' + +module.exports = { + site: 'magentatv.at', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `${API_STATIC_ENDPOINT}/de/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(6, 'h').format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(12, 'h').format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/de/events/segments/${date.add(18, 'h').format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.title, + sub_title: detail.episodeName, + description: detail.longDescription, + date: detail.productionDate, + category: detail.genres, + actors: detail.actors, + directors: detail.directors, + producers: detail.producers, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=de&productClass=Orion-DASH`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'de', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.id) return {} + const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} diff --git a/sites/magentatv.at/magentatv.at.test.js b/sites/magentatv.at/magentatv.at.test.js index de0c538a..bebf091e 100644 --- a/sites/magentatv.at/magentatv.at.test.js +++ b/sites/magentatv.at/magentatv.at.test.js @@ -1,97 +1,97 @@ -// npm run channels:parse -- --config=./sites/magentatv.at/magentatv.at.config.js --output=./sites/magentatv.at/magentatv.at.channels.xml -// npm run grab -- --site=magentatv.at - -const { parser, url } = require('./magentatv.at.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' -const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '13TH_STREET_HD', - xmltv_id: '13thStreet.de', - lang: 'de' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe(`${API_STATIC_ENDPOINT}/de/events/segments/20221030000000`) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030060000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030120000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030180000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2236391~~2FEP019388320252,imi:af4af994f29354e64878101c0612b17999d0c1a3?returnLinearContent=true` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T23:55:00.000Z', - stop: '2022-10-30T01:40:00.000Z', - title: 'Law & Order: Special Victims Unit', - sub_title: 'Mutterinstinkt', - description: - 'Patty Branson wird von einem Jungen in einem Park angegriffen und von diesem verfolgt. Der Junge wurde von Michelle Osborne engagiert, die vorgibt, die leibliche Mutter des Mädchens zu sein. Doch ist dies tatsächlich die Wahrheit?', - date: '2004', - category: ['Drama-Serie', 'Krimi Drama', 'Action', 'Thriller'], - actors: [ - 'Christopher Meloni', - 'Mariska Hargitay', - 'Richard Belzer', - 'Dann Florek', - 'Ice-T', - 'BD Wong', - 'Diane Neal', - 'Tamara Tunie', - 'Abigail Breslin', - 'Lea Thompson' - ], - directors: ['Arthur W. Forney'], - producers: ['Dick Wolf', 'Ted Kotcheff', 'Neal Baer'], - season: 6, - episode: 1 - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/magentatv.at/magentatv.at.config.js --output=./sites/magentatv.at/magentatv.at.channels.xml +// npm run grab -- --site=magentatv.at + +const { parser, url } = require('./magentatv.at.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const API_STATIC_ENDPOINT = 'https://static.spark.magentatv.at/deu/web/epg-service-lite/at' +const API_PROD_ENDPOINT = 'https://prod.spark.magentatv.at/deu/web/linear-service/v2' + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '13TH_STREET_HD', + xmltv_id: '13thStreet.de', + lang: 'de' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe(`${API_STATIC_ENDPOINT}/de/events/segments/20221030000000`) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030060000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030120000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/de/events/segments/20221030180000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2236391~~2FEP019388320252,imi:af4af994f29354e64878101c0612b17999d0c1a3?returnLinearContent=true` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T23:55:00.000Z', + stop: '2022-10-30T01:40:00.000Z', + title: 'Law & Order: Special Victims Unit', + sub_title: 'Mutterinstinkt', + description: + 'Patty Branson wird von einem Jungen in einem Park angegriffen und von diesem verfolgt. Der Junge wurde von Michelle Osborne engagiert, die vorgibt, die leibliche Mutter des Mädchens zu sein. Doch ist dies tatsächlich die Wahrheit?', + date: '2004', + category: ['Drama-Serie', 'Krimi Drama', 'Action', 'Thriller'], + actors: [ + 'Christopher Meloni', + 'Mariska Hargitay', + 'Richard Belzer', + 'Dann Florek', + 'Ice-T', + 'BD Wong', + 'Diane Neal', + 'Tamara Tunie', + 'Abigail Breslin', + 'Lea Thompson' + ], + directors: ['Arthur W. Forney'], + producers: ['Dick Wolf', 'Ted Kotcheff', 'Neal Baer'], + season: 6, + episode: 1 + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/magentatv.de/magentatv.de.config.js b/sites/magentatv.de/magentatv.de.config.js index 4edefacc..ad7f191a 100644 --- a/sites/magentatv.de/magentatv.de.config.js +++ b/sites/magentatv.de/magentatv.de.config.js @@ -1,123 +1,123 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const X_CSRFTOKEN = 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe' -const COOKIE = - 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' - -module.exports = { - site: 'magentatv.de', - days: 2, - url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList', - request: { - method: 'POST', - headers: { - X_CSRFToken: X_CSRFTOKEN, - 'Content-Type': 'application/json', - Cookie: COOKIE - }, - data({ channel, date }) { - return { - count: -1, - isFillProgram: 1, - offset: 0, - properties: [ - { - include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', - name: 'playbill' - } - ], - type: 2, - begintime: date.format('YYYYMMDD000000'), - channelid: channel.site_id, - endtime: date.add(1, 'd').format('YYYYMMDD000000') - } - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.introduce, - icon: parseIcon(item), - category: parseCategory(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post( - 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel', - { - channelNamespace: 2, - filterlist: [ - { - key: 'IsHide', - value: '-1' - } - ], - metaDataVer: 'Channel/1.1', - properties: [ - { - include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name', - name: 'logicalChannel' - } - ], - returnSatChannel: 0 - }, - { - headers: { - X_CSRFToken: X_CSRFTOKEN, - 'Content-Type': 'application/json', - Cookie: COOKIE - } - } - ) - .then(r => r.data) - .catch(console.log) - - return data.channellist.map(item => { - return { - lang: 'de', - site_id: item.contentId, - name: item.name - } - }) - } -} - -function parseCategory(item) { - return item.genres - ? item.genres - .replace('und', ',') - .split(',') - .map(i => i.trim()) - : [] -} - -function parseIcon(item) { - if (!Array.isArray(item.pictures) || !item.pictures.length) return null - - return item.pictures[0].href -} - -function parseStart(item) { - return dayjs(item.starttime) -} - -function parseStop(item) { - return dayjs(item.endtime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.playbilllist)) return [] - - return data.playbilllist -} +const axios = require('axios') +const dayjs = require('dayjs') + +const X_CSRFTOKEN = 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe' +const COOKIE = + 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' + +module.exports = { + site: 'magentatv.de', + days: 2, + url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList', + request: { + method: 'POST', + headers: { + X_CSRFToken: X_CSRFTOKEN, + 'Content-Type': 'application/json', + Cookie: COOKIE + }, + data({ channel, date }) { + return { + count: -1, + isFillProgram: 1, + offset: 0, + properties: [ + { + include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', + name: 'playbill' + } + ], + type: 2, + begintime: date.format('YYYYMMDD000000'), + channelid: channel.site_id, + endtime: date.add(1, 'd').format('YYYYMMDD000000') + } + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.introduce, + icon: parseIcon(item), + category: parseCategory(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post( + 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel', + { + channelNamespace: 2, + filterlist: [ + { + key: 'IsHide', + value: '-1' + } + ], + metaDataVer: 'Channel/1.1', + properties: [ + { + include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name', + name: 'logicalChannel' + } + ], + returnSatChannel: 0 + }, + { + headers: { + X_CSRFToken: X_CSRFTOKEN, + 'Content-Type': 'application/json', + Cookie: COOKIE + } + } + ) + .then(r => r.data) + .catch(console.log) + + return data.channellist.map(item => { + return { + lang: 'de', + site_id: item.contentId, + name: item.name + } + }) + } +} + +function parseCategory(item) { + return item.genres + ? item.genres + .replace('und', ',') + .split(',') + .map(i => i.trim()) + : [] +} + +function parseIcon(item) { + if (!Array.isArray(item.pictures) || !item.pictures.length) return null + + return item.pictures[0].href +} + +function parseStart(item) { + return dayjs(item.starttime) +} + +function parseStop(item) { + return dayjs(item.endtime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.playbilllist)) return [] + + return data.playbilllist +} diff --git a/sites/magentatv.de/magentatv.de.test.js b/sites/magentatv.de/magentatv.de.test.js index 42a30a11..9fbf8bf9 100644 --- a/sites/magentatv.de/magentatv.de.test.js +++ b/sites/magentatv.de/magentatv.de.test.js @@ -1,79 +1,79 @@ -// npm run channels:parse -- --config=./sites/magentatv.de/magentatv.de.config.js --output=./sites/magentatv.de/magentatv.de.channels.xml -// npm run grab -- --site=magentatv.de - -const { parser, url, request } = require('./magentatv.de.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-09', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '255', - xmltv_id: '13thStreet.de' -} - -it('can generate valid url', () => { - expect(url).toBe('https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - X_CSRFToken: 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe', - 'Content-Type': 'application/json', - Cookie: - 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - count: -1, - isFillProgram: 1, - offset: 0, - properties: [ - { - include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', - name: 'playbill' - } - ], - type: 2, - begintime: '20220309000000', - channelid: '255', - endtime: '20220310000000' - }) -}) - -it('can parse response', () => { - const content = - '{"playbilllist":[{"id":"30021745","name":"FBI: Special Crime Unit","introduce":"Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.","channelid":"255","starttime":"2022-03-09 01:00:00 UTC+01:00","endtime":"2022-03-09 01:45:00 UTC+01:00","genres":"Wissen,Natur und Tiere","pictures":[{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg","description":"Brother\'s Keeper","imageType":"1","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h9_ae.jpg","description":"FBI","imageType":"13","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h8_af.jpg","description":"Brother\'s Keeper","imageType":"17","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h10_af.jpg","description":"FBI","imageType":"18","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]}]}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-09T00:00:00.000Z', - stop: '2022-03-09T00:45:00.000Z', - title: 'FBI: Special Crime Unit', - description: - 'Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.', - icon: 'http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg', - category: ['Wissen', 'Natur', 'Tiere'] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"counttotal":"0"}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/magentatv.de/magentatv.de.config.js --output=./sites/magentatv.de/magentatv.de.channels.xml +// npm run grab -- --site=magentatv.de + +const { parser, url, request } = require('./magentatv.de.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-09', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '255', + xmltv_id: '13thStreet.de' +} + +it('can generate valid url', () => { + expect(url).toBe('https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + X_CSRFToken: 'e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe', + 'Content-Type': 'application/json', + Cookie: + 'JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6; CSESSIONID=4A36799EF09D80539BBA8E8211FA80D3; CSRFSESSION=e0a032d1c9df6c3fb8c8352399d32c40ddb17ccceb5142fe; JSESSIONID=93892A98DBCCEBD83EDC4C23EBEB23B6' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + count: -1, + isFillProgram: 1, + offset: 0, + properties: [ + { + include: 'endtime,genres,id,name,starttime,channelid,pictures,introduce', + name: 'playbill' + } + ], + type: 2, + begintime: '20220309000000', + channelid: '255', + endtime: '20220310000000' + }) +}) + +it('can parse response', () => { + const content = + '{"playbilllist":[{"id":"30021745","name":"FBI: Special Crime Unit","introduce":"Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.","channelid":"255","starttime":"2022-03-09 01:00:00 UTC+01:00","endtime":"2022-03-09 01:45:00 UTC+01:00","genres":"Wissen,Natur und Tiere","pictures":[{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg","description":"Brother\'s Keeper","imageType":"1","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h9_ae.jpg","description":"FBI","imageType":"13","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1440","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h8_af.jpg","description":"Brother\'s Keeper","imageType":"17","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]},{"rel":"image","href":"http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/5/p15528073_i_h10_af.jpg","description":"FBI","imageType":"18","copyrightNotice":"(c) ProSiebenSat.1","mimeType":"image/jpeg","resolution":["1920","1080"]}]}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-09T00:00:00.000Z', + stop: '2022-03-09T00:45:00.000Z', + title: 'FBI: Special Crime Unit', + description: + 'Nachdem ein Mann von einem Sprengstoffpaket getötet wurde, das zu ihm nach Hause geschickt wurde, versucht das Team, den Absender zu fassen und sein neuestes tödliches Paket abzufangen. Maggie hat Mühe, ihrer jüngeren Schwester zu vertrauen.', + icon: 'http://ngiss.t-online.de/sweetprogrammanager/media/gracenote/1/9/p19740197_e_h9_af.jpg', + category: ['Wissen', 'Natur', 'Tiere'] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"counttotal":"0"}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/magticom.ge/magticom.ge.config.js b/sites/magticom.ge/magticom.ge.config.js index 61044471..0e5a475d 100644 --- a/sites/magticom.ge/magticom.ge.config.js +++ b/sites/magticom.ge/magticom.ge.config.js @@ -1,86 +1,86 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'magticom.ge', - days: 2, - url: 'https://www.magticom.ge/request/channel-program.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('channelId', channel.site_id) - params.append('start', date.unix()) - params.append('end', date.add(1, 'd').unix()) - - return params - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.info, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.magticom.ge/en/tv/tv-services/tv-guide') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const channels = $( - '#article > article > div > div > div.tv-guide > div.tv-guide-channels > div.tv-guide-channel' - ).toArray() - - return channels.map(item => { - const $item = cheerio.load(item) - const channelId = $item('*').data('id') - return { - lang: 'ka', - site_id: channelId, - name: $item('.tv-guide-channel-title > div > div').text() - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.startTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') -} - -function parseStop(item) { - return dayjs.tz(item.endTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch (error) { - console.log(error.message) - } - if (!data || !Array.isArray(data)) return [] - - return data -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'magticom.ge', + days: 2, + url: 'https://www.magticom.ge/request/channel-program.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('channelId', channel.site_id) + params.append('start', date.unix()) + params.append('end', date.add(1, 'd').unix()) + + return params + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.info, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.magticom.ge/en/tv/tv-services/tv-guide') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const channels = $( + '#article > article > div > div > div.tv-guide > div.tv-guide-channels > div.tv-guide-channel' + ).toArray() + + return channels.map(item => { + const $item = cheerio.load(item) + const channelId = $item('*').data('id') + return { + lang: 'ka', + site_id: channelId, + name: $item('.tv-guide-channel-title > div > div').text() + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.startTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') +} + +function parseStop(item) { + return dayjs.tz(item.endTimestamp, 'YYYY-MM-DDTHH:mm:ss', 'Asia/Tbilisi') +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch (error) { + console.log(error.message) + } + if (!data || !Array.isArray(data)) return [] + + return data +} diff --git a/sites/magticom.ge/magticom.ge.test.js b/sites/magticom.ge/magticom.ge.test.js index 9282ebde..9b3c82cd 100644 --- a/sites/magticom.ge/magticom.ge.test.js +++ b/sites/magticom.ge/magticom.ge.test.js @@ -1,66 +1,66 @@ -// npm run channels:parse -- --config=./sites/magticom.ge/magticom.ge.config.js --output=./sites/magticom.ge/magticom.ge.channels.xml -// npm run grab -- --site=magticom.ge - -const { parser, url, request } = require('./magticom.ge.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '260', - xmltv_id: 'BollywoodHDRussia.ru' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.magticom.ge/request/channel-program.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.has('channelId')).toBe(true) - expect(result.has('start')).toBe(true) - expect(result.has('end')).toBe(true) -}) - -it('can parse response', () => { - const content = - '[{"id":2313254118,"channelId":260,"startTimestamp":"2021-11-22T07:00:00","endTimestamp":"2021-11-22T09:00:00","duration":null,"title":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","subTitle":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","info":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434.","pg":null,"year":null,"country":null,"imageUrl":null,"createdBy":-200,"creationTimestamp":"2021-11-21T18:04:52","epgSourceId":8,"startDateStr":"20211122070000","genreByGenreId":null,"languageByLanguageId":{"id":3,"name":"\\u10e0\\u10e3\\u10e1\\u10e3\\u10da\\u10d8","orderIndex":3,"nameShort":"ru"},"externalId":"2021460000084132","programHumanById":[],"date":null,"time":null,"startDate":null,"endDate":null,"longInfo":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434."}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-22T03:00:00.000Z', - stop: '2021-11-22T05:00:00.000Z', - title: 'Х/ф "Неравный брак".', - description: - 'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/magticom.ge/magticom.ge.config.js --output=./sites/magticom.ge/magticom.ge.channels.xml +// npm run grab -- --site=magticom.ge + +const { parser, url, request } = require('./magticom.ge.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '260', + xmltv_id: 'BollywoodHDRussia.ru' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.magticom.ge/request/channel-program.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: 'https://www.magticom.ge/en/tv/tv-services/tv-guide' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.has('channelId')).toBe(true) + expect(result.has('start')).toBe(true) + expect(result.has('end')).toBe(true) +}) + +it('can parse response', () => { + const content = + '[{"id":2313254118,"channelId":260,"startTimestamp":"2021-11-22T07:00:00","endTimestamp":"2021-11-22T09:00:00","duration":null,"title":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","subTitle":"\\u0425\\/\\u0444 \\"\\u041d\\u0435\\u0440\\u0430\\u0432\\u043d\\u044b\\u0439 \\u0431\\u0440\\u0430\\u043a\\".","info":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434.","pg":null,"year":null,"country":null,"imageUrl":null,"createdBy":-200,"creationTimestamp":"2021-11-21T18:04:52","epgSourceId":8,"startDateStr":"20211122070000","genreByGenreId":null,"languageByLanguageId":{"id":3,"name":"\\u10e0\\u10e3\\u10e1\\u10e3\\u10da\\u10d8","orderIndex":3,"nameShort":"ru"},"externalId":"2021460000084132","programHumanById":[],"date":null,"time":null,"startDate":null,"endDate":null,"longInfo":"\\u0413\\u0443\\u0434\\u0436\\u0430\\u0440\\u0430\\u0442\\u0435\\u0446 \\u0425\\u0430\\u0441\\u043c\\u0443\\u043a\\u0445 \\u041f\\u0430\\u0442\\u0435\\u043b \\u043f\\u043e\\u0441\\u0441\\u043e\\u0440\\u0438\\u043b\\u0441\\u044f \\u0441 \\u043d\\u043e\\u0432\\u044b\\u043c \\u0441\\u043e\\u0441\\u0435\\u0434\\u043e\\u043c \\u0413\\u0443\\u0433\\u0433\\u0438 \\u0422\\u0430\\u043d\\u0434\\u043e\\u043d\\u043e\\u043c. \\u041d\\u043e \\u0438\\u043c \\u043f\\u0440\\u0438\\u0445\\u043e\\u0434\\u0438\\u0442\\u0441\\u044f \\u043f\\u043e\\u043c\\u0438\\u0440\\u0438\\u0442\\u044c\\u0441\\u044f, \\u043a\\u043e\\u0433\\u0434\\u0430 \\u0438\\u0445 \\u0434\\u0435\\u0442\\u0438 \\u0432\\u043b\\u044e\\u0431\\u043b\\u044f\\u044e\\u0442\\u0441\\u044f \\u0434\\u0440\\u0443\\u0433 \\u0432 \\u0434\\u0440\\u0443\\u0433\\u0430. \\u0420\\u0435\\u0436\\u0438\\u0441\\u0441\\u0435\\u0440: \\u0421\\u0430\\u043d\\u0434\\u0436\\u0430\\u0439 \\u0427\\u0445\\u0435\\u043b. \\u0410\\u043a\\u0442\\u0435\\u0440\\u044b: \\u0420\\u0438\\u0448\\u0438 \\u041a\\u0430\\u043f\\u0443\\u0440, \\u041f\\u0430\\u0440\\u0435\\u0448 \\u0420\\u0430\\u0432\\u0430\\u043b, \\u0412\\u0438\\u0440 \\u0414\\u0430\\u0441. 2017 \\u0433\\u043e\\u0434."}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-22T03:00:00.000Z', + stop: '2021-11-22T05:00:00.000Z', + title: 'Х/ф "Неравный брак".', + description: + 'Гуджаратец Хасмукх Пател поссорился с новым соседом Гугги Тандоном. Но им приходится помириться, когда их дети влюбляются друг в друга. Режиссер: Санджай Чхел. Актеры: Риши Капур, Пареш Равал, Вир Дас. 2017 год.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mako.co.il/mako.co.il.config.js b/sites/mako.co.il/mako.co.il.config.js index 0466dfb6..73474d11 100644 --- a/sites/mako.co.il/mako.co.il.config.js +++ b/sites/mako.co.il/mako.co.il.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mako.co.il', - days: 2, - url: 'https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp', - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(item.DurationMs, 'ms') - programs.push({ - title: item.ProgramName, - description: item.EventDescription, - icon: item.Picture, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.StartTimeUTC) return null - - return dayjs(item.StartTimeUTC) -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programs)) return [] - const d = date.format('DD/MM/YYYY') - - return data.programs.filter(item => item.Date.startsWith(d)) -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mako.co.il', + days: 2, + url: 'https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp', + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(item.DurationMs, 'ms') + programs.push({ + title: item.ProgramName, + description: item.EventDescription, + icon: item.Picture, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.StartTimeUTC) return null + + return dayjs(item.StartTimeUTC) +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programs)) return [] + const d = date.format('DD/MM/YYYY') + + return data.programs.filter(item => item.Date.startsWith(d)) +} diff --git a/sites/mako.co.il/mako.co.il.test.js b/sites/mako.co.il/mako.co.il.test.js index da82beca..5cb503dc 100644 --- a/sites/mako.co.il/mako.co.il.test.js +++ b/sites/mako.co.il/mako.co.il.test.js @@ -1,42 +1,42 @@ -// npm run grab -- --site=mako.co.il - -const { parser, url } = require('./mako.co.il.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-07', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp') -}) - -it('can parse response', () => { - const content = - '{"programs":[{"DisplayEndTime":"06:15","MakoTVURL":"","HouseNumber":"L17165475","StartTimeUTC":1646539200000,"DurationMs":900000,"DisplayStartTime":"06:00","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"06/03/2022 06:00:00","RerunBroadcast":false,"Duration":"00:15","ProgramName":"כותרות הבוקר","Date":"06/03/2022 06:00:00","MakoProgramsURL":"","LiveBroadcast":true,"ProgramCode":134987,"Episode":"","Picture":"https://img.mako.co.il//2021/08/04/hadshot_haboker_im_niv_raskin.jpg","MakoShortName":"","hebrewDate":"6 במרץ","Season":"","day":"הערב","EventDescription":"","EnglishName":"cotrot,EP 46"},{"DisplayEndTime":"02:39","MakoTVURL":"","HouseNumber":"A168960","StartTimeUTC":1646613480000,"DurationMs":60000,"DisplayStartTime":"02:38","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"07/03/2022 02:38:00","RerunBroadcast":true,"Duration":"00:01","ProgramName":"רוקדים עם כוכבים - בר זומר","Date":"07/03/2022 02:38:00","MakoProgramsURL":"","LiveBroadcast":false,"ProgramCode":135029,"Episode":"","Picture":"https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg","MakoShortName":"","hebrewDate":"7 במרץ","Season":"","day":"מחר","EventDescription":"מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.","EnglishName":"rokdim,EP 10"}]}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-07T00:38:00.000Z', - stop: '2022-03-07T00:39:00.000Z', - title: 'רוקדים עם כוכבים - בר זומר', - description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.', - icon: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mako.co.il + +const { parser, url } = require('./mako.co.il.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-07', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url).toBe('https://www.mako.co.il/AjaxPage?jspName=EPGResponse.jsp') +}) + +it('can parse response', () => { + const content = + '{"programs":[{"DisplayEndTime":"06:15","MakoTVURL":"","HouseNumber":"L17165475","StartTimeUTC":1646539200000,"DurationMs":900000,"DisplayStartTime":"06:00","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"06/03/2022 06:00:00","RerunBroadcast":false,"Duration":"00:15","ProgramName":"כותרות הבוקר","Date":"06/03/2022 06:00:00","MakoProgramsURL":"","LiveBroadcast":true,"ProgramCode":134987,"Episode":"","Picture":"https://img.mako.co.il//2021/08/04/hadshot_haboker_im_niv_raskin.jpg","MakoShortName":"","hebrewDate":"6 במרץ","Season":"","day":"הערב","EventDescription":"","EnglishName":"cotrot,EP 46"},{"DisplayEndTime":"02:39","MakoTVURL":"","HouseNumber":"A168960","StartTimeUTC":1646613480000,"DurationMs":60000,"DisplayStartTime":"02:38","MobilePicture":"https://img.mako.co.il/2017/01/01/placeHolder.jpg","StartTime":"07/03/2022 02:38:00","RerunBroadcast":true,"Duration":"00:01","ProgramName":"רוקדים עם כוכבים - בר זומר","Date":"07/03/2022 02:38:00","MakoProgramsURL":"","LiveBroadcast":false,"ProgramCode":135029,"Episode":"","Picture":"https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg","MakoShortName":"","hebrewDate":"7 במרץ","Season":"","day":"מחר","EventDescription":"מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.","EnglishName":"rokdim,EP 10"}]}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-07T00:38:00.000Z', + stop: '2022-03-07T00:39:00.000Z', + title: 'רוקדים עם כוכבים - בר זומר', + description: 'מהדורת החדשות המרכזית של הבוקר, האנשים הפרשנויות והכותרות שיעשו את היום.', + icon: 'https://img.mako.co.il/2022/02/13/DancingWithStars2022_EPG.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js index c3d29d5f..014d7012 100644 --- a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js +++ b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'maxtv.hrvatskitelekom.hr', - days: 2, - url: 'https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows', - request: { - method: 'POST', - data: function ({ channel, date }) { - return { - channelList: [channel.site_id], - startDate: date.unix(), - endDate: date.add(1, 'd').unix() - } - } - }, - parser: function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (item.showId == -1) return - programs.push({ - title: item.title, - category: item.category, - start: dayjs.unix(item.startTime), - stop: dayjs.unix(item.endTime) - }) - }) - - return programs - } -} - -function parseContent(content, channel) { - const json = JSON.parse(content) - if (!Array.isArray(json.data)) return null - - return json.data.find(i => i.channelId == channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - - return data && Array.isArray(data.shows) ? data.shows : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'maxtv.hrvatskitelekom.hr', + days: 2, + url: 'https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows', + request: { + method: 'POST', + data: function ({ channel, date }) { + return { + channelList: [channel.site_id], + startDate: date.unix(), + endDate: date.add(1, 'd').unix() + } + } + }, + parser: function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (item.showId == -1) return + programs.push({ + title: item.title, + category: item.category, + start: dayjs.unix(item.startTime), + stop: dayjs.unix(item.endTime) + }) + }) + + return programs + } +} + +function parseContent(content, channel) { + const json = JSON.parse(content) + if (!Array.isArray(json.data)) return null + + return json.data.find(i => i.channelId == channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + + return data && Array.isArray(data.shows) ? data.shows : [] +} diff --git a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js index 6388c9b7..fb9c6679 100644 --- a/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js +++ b/sites/maxtv.hrvatskitelekom.hr/maxtv.hrvatskitelekom.hr.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=maxtv.hrvatskitelekom.hr - -const { parser, url, request } = require('./maxtv.hrvatskitelekom.hr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '316', - xmltv_id: '24KitchenCroatia.hr' -} -const content = - '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[{"channelId":"316","title":"24Kitchen","logo":"http://ottepg5.nexttv.ht.hr:33200/EPG/jsp/images/universal/film/logo/fileEntity/20161109/000200/XTV100002173/493d03f8-0f08-4932-8371-e5b57d96f17d.png","chanNumber":500,"hasCatchup":false,"ottChannel":true,"userSubscribed":false,"shows":[{"showId":"-1","title":"Nema informacija","startTime":1636952400,"endTime":1636967400,"category":"ostalo","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""},{"showId":"17298142","title":"Najčudniji svjetski restorani","startTime":1636952400,"endTime":1636952700,"category":"Kulinarski","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""}]}]}' - -it('can generate valid url', () => { - expect(url).toBe('https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows') -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - channelList: ['316'], - startDate: 1637020800, - endDate: 1637107200 - }) -}) - -it('can parse response', () => { - const result = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-15T05:00:00.000Z', - stop: '2021-11-15T05:05:00.000Z', - title: 'Najčudniji svjetski restorani', - category: 'Kulinarski' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=maxtv.hrvatskitelekom.hr + +const { parser, url, request } = require('./maxtv.hrvatskitelekom.hr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '316', + xmltv_id: '24KitchenCroatia.hr' +} +const content = + '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[{"channelId":"316","title":"24Kitchen","logo":"http://ottepg5.nexttv.ht.hr:33200/EPG/jsp/images/universal/film/logo/fileEntity/20161109/000200/XTV100002173/493d03f8-0f08-4932-8371-e5b57d96f17d.png","chanNumber":500,"hasCatchup":false,"ottChannel":true,"userSubscribed":false,"shows":[{"showId":"-1","title":"Nema informacija","startTime":1636952400,"endTime":1636967400,"category":"ostalo","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""},{"showId":"17298142","title":"Najčudniji svjetski restorani","startTime":1636952400,"endTime":1636952700,"category":"Kulinarski","hasReminder":false,"hasRecording":false,"hasSeriesRecording":false,"userOttPlayable":false,"userLocked":false,"isPPV":false,"buyPrice":""}]}]}' + +it('can generate valid url', () => { + expect(url).toBe('https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/shows') +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + channelList: ['316'], + startDate: 1637020800, + endDate: 1637107200 + }) +}) + +it('can parse response', () => { + const result = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-15T05:00:00.000Z', + stop: '2021-11-15T05:05:00.000Z', + title: 'Najčudniji svjetski restorani', + category: 'Kulinarski' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"status":{"code":200,"message":"OK","authType":"Unauthenticated","ottSessionToken":null},"data":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/maxtvgo.mk/maxtvgo.mk.config.js b/sites/maxtvgo.mk/maxtvgo.mk.config.js index 83991ae0..aa5df0ae 100644 --- a/sites/maxtvgo.mk/maxtvgo.mk.config.js +++ b/sites/maxtvgo.mk/maxtvgo.mk.config.js @@ -1,72 +1,72 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) - -module.exports = { - site: 'maxtvgo.mk', - days: 2, - url: function ({ channel, date }) { - return `https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/${ - channel.site_id - }/start/${date.format('YYYYMMDDHHmmss')}/stop/${date - .add(1, 'd') - .format('YYYYMMDDHHmmss')}/include_current/true/format/json` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - category: item.category, - description: parseDescription(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get( - 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/channel/all/application_id/deep_blue/device_configuration/2/instance_id/1/language/mk/http_proto/https/format/json' - ) - .then(r => r.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'mk', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseStart(item) { - return dayjs(item['@attributes'].start, 'YYYYMMDDHHmmss ZZ') -} - -function parseStop(item) { - return dayjs(item['@attributes'].stop, 'YYYYMMDDHHmmss ZZ') -} - -function parseDescription(item) { - return typeof item.desc === 'string' ? item.desc : null -} - -function parseIcon(item) { - return item.icon['@attributes'].src -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programme)) return [] - - return data.programme -} +const axios = require('axios') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) + +module.exports = { + site: 'maxtvgo.mk', + days: 2, + url: function ({ channel, date }) { + return `https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/${ + channel.site_id + }/start/${date.format('YYYYMMDDHHmmss')}/stop/${date + .add(1, 'd') + .format('YYYYMMDDHHmmss')}/include_current/true/format/json` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + category: item.category, + description: parseDescription(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get( + 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/channel/all/application_id/deep_blue/device_configuration/2/instance_id/1/language/mk/http_proto/https/format/json' + ) + .then(r => r.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'mk', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseStart(item) { + return dayjs(item['@attributes'].start, 'YYYYMMDDHHmmss ZZ') +} + +function parseStop(item) { + return dayjs(item['@attributes'].stop, 'YYYYMMDDHHmmss ZZ') +} + +function parseDescription(item) { + return typeof item.desc === 'string' ? item.desc : null +} + +function parseIcon(item) { + return item.icon['@attributes'].src +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programme)) return [] + + return data.programme +} diff --git a/sites/maxtvgo.mk/maxtvgo.mk.test.js b/sites/maxtvgo.mk/maxtvgo.mk.test.js index 18c7fa97..fb95a1eb 100644 --- a/sites/maxtvgo.mk/maxtvgo.mk.test.js +++ b/sites/maxtvgo.mk/maxtvgo.mk.test.js @@ -1,73 +1,73 @@ -// npm run channels:parse -- --config=./sites/maxtvgo.mk/maxtvgo.mk.config.js --output=./sites/maxtvgo.mk/maxtvgo.mk.channels.xml -// npm run grab -- --site=maxtvgo.mk - -const { parser, url } = require('./maxtvgo.mk.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '105', - xmltv_id: 'MRT1.mk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json' - ) -}) - -it('can parse response', () => { - const content = - '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":"Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.","icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T22:10:00.000Z', - stop: '2021-11-17T00:00:00.000Z', - title: 'Палмето - игран филм', - category: 'Останато', - description: - 'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.', - icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' - } - ]) -}) - -it('can parse response with no description', () => { - const content = - '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":{},"icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T22:10:00.000Z', - stop: '2021-11-17T00:00:00.000Z', - title: 'Палмето - игран филм', - category: 'Останато', - description: null, - icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"@attributes":{"source-info-name":"maxtvgo.mk","generator-info-name":"spectar_epg"}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/maxtvgo.mk/maxtvgo.mk.config.js --output=./sites/maxtvgo.mk/maxtvgo.mk.channels.xml +// npm run grab -- --site=maxtvgo.mk + +const { parser, url } = require('./maxtvgo.mk.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '105', + xmltv_id: 'MRT1.mk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://prd-static-mkt.spectar.tv/rev-1636968171/client_api.php/epg/list/instance_id/1/language/mk/channel_id/105/start/20211117000000/stop/20211118000000/include_current/true/format/json' + ) +}) + +it('can parse response', () => { + const content = + '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":"Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.","icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T22:10:00.000Z', + stop: '2021-11-17T00:00:00.000Z', + title: 'Палмето - игран филм', + category: 'Останато', + description: + 'Екстремниот рибар, Џереми Вејд, е во потрага по слатководни риби кои јадат човечко месо. Со форензички методи, Џереми им илустрира на гледачите како овие нови чудовишта се создадени да убиваат.', + icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' + } + ]) +}) + +it('can parse response with no description', () => { + const content = + '{"programme":[{"@attributes":{"channel":"105","id":"21949063","start":"20211116231000 +0100","stop":"20211117010000 +0100","disable_catchup":"0","is_adult":"0"},"title":"Палмето - игран филм","original-title":{"@attributes":{"lang":""}},"sub-title":{"@attributes":{"lang":""}},"category_id":"11","category":"Останато","desc":{},"icon":{"@attributes":{"src":"https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1"}},"episode_num":{},"date":"0","star-rating":{"value":{}},"rating":{"@attributes":{"system":""},"value":"0+"},"linear_channel_rating":"0+","genres":{},"credits":{}}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T22:10:00.000Z', + stop: '2021-11-17T00:00:00.000Z', + title: 'Палмето - игран филм', + category: 'Останато', + description: null, + icon: 'https://prd-static-mkt.spectar.tv/rev-1636968170/image_transform.php/transform/1/epg_program_id/21949063/instance_id/1' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"@attributes":{"source-info-name":"maxtvgo.mk","generator-info-name":"spectar_epg"}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mbc.net/mbc.net.config.js b/sites/mbc.net/mbc.net.config.js index b8ddf0ed..8e2b8d88 100644 --- a/sites/mbc.net/mbc.net.config.js +++ b/sites/mbc.net/mbc.net.config.js @@ -1,40 +1,40 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'mbc.net', - days: 2, - url({ date, channel }) { - return `https://www.mbc.net/.rest/api/channel/grids?from=${date.valueOf()}&to=${date - .add(1, 'd') - .valueOf()}&channel=${channel.site_id}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.showPageTitle, - category: item.showPageGenreInArabic, - description: item.showPageAboutInArabic, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs(item.startTime).toJSON() -} - -function parseStop(item) { - return dayjs(item.endTime).toJSON() -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data -} +const dayjs = require('dayjs') + +module.exports = { + site: 'mbc.net', + days: 2, + url({ date, channel }) { + return `https://www.mbc.net/.rest/api/channel/grids?from=${date.valueOf()}&to=${date + .add(1, 'd') + .valueOf()}&channel=${channel.site_id}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.showPageTitle, + category: item.showPageGenreInArabic, + description: item.showPageAboutInArabic, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs(item.startTime).toJSON() +} + +function parseStop(item) { + return dayjs(item.endTime).toJSON() +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data +} diff --git a/sites/mbc.net/mbc.net.test.js b/sites/mbc.net/mbc.net.test.js index 554e61c0..23318c2f 100644 --- a/sites/mbc.net/mbc.net.test.js +++ b/sites/mbc.net/mbc.net.test.js @@ -1,46 +1,46 @@ -// npm run grab -- --site=mbc.net - -const { parser, url } = require('./mbc.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'mbc1', - xmltv_id: 'MBC.ae' -} -const content = - '[{"id":3140240,"channelBCMId":"1","channelLabel":"MBC1","showPageTitle":"اختطاف","showPageGenreInArabic":" دراما","showPageAboutInArabic":".يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع","startTime":1636155131000,"endTime":1636157806000,"startTimeInMilliseconds":1636155131000,"endTimeInMilliseconds":1636157806200,"premiereMode":"Fast Repeat","showingNow":false}]' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.mbc.net/.rest/api/channel/grids?from=1636156800000&to=1636243200000&channel=mbc1' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-05T23:32:11.000Z', - stop: '2021-11-06T00:16:46.000Z', - title: 'اختطاف', - category: ' دراما', - description: - '.يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mbc.net + +const { parser, url } = require('./mbc.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'mbc1', + xmltv_id: 'MBC.ae' +} +const content = + '[{"id":3140240,"channelBCMId":"1","channelLabel":"MBC1","showPageTitle":"اختطاف","showPageGenreInArabic":" دراما","showPageAboutInArabic":".يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع","startTime":1636155131000,"endTime":1636157806000,"startTimeInMilliseconds":1636155131000,"endTimeInMilliseconds":1636157806200,"premiereMode":"Fast Repeat","showingNow":false}]' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.mbc.net/.rest/api/channel/grids?from=1636156800000&to=1636243200000&channel=mbc1' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-05T23:32:11.000Z', + stop: '2021-11-06T00:16:46.000Z', + title: 'اختطاف', + category: ' دراما', + description: + '.يستضيف برنامج تلفزيوني والدة لينا وشقيقتها، ولدى مشاهدتها الحلقة، تكتشف والدة ماجد الحقيقة، بينما تتعرض العنود لحادث سير مروع' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mediagenie.co.kr/mediagenie.co.kr.config.js b/sites/mediagenie.co.kr/mediagenie.co.kr.config.js index 32f85cbe..35fcb007 100644 --- a/sites/mediagenie.co.kr/mediagenie.co.kr.config.js +++ b/sites/mediagenie.co.kr/mediagenie.co.kr.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mediagenie.co.kr', - days: 1, - url({ channel, date }) { - return `https://mediagenie.co.kr/${channel.site_id}/?qd=${date.format('YYYYMMDD')}` - }, - request: { - headers: { - cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - rating: parseRating($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.col2').clone().children().remove().end().text().trim() -} - -function parseRating($item) { - const rating = $item('.col6').text().trim() - - return rating - ? { - system: 'KMRB', - value: rating - } - : null -} - -function parseStart($item, date) { - const time = $item('.col1').text().trim() - - if (!/^\d{2}:\d{2}$/.test(time)) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.tbl > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mediagenie.co.kr', + days: 1, + url({ channel, date }) { + return `https://mediagenie.co.kr/${channel.site_id}/?qd=${date.format('YYYYMMDD')}` + }, + request: { + headers: { + cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + rating: parseRating($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.col2').clone().children().remove().end().text().trim() +} + +function parseRating($item) { + const rating = $item('.col6').text().trim() + + return rating + ? { + system: 'KMRB', + value: rating + } + : null +} + +function parseStart($item, date) { + const time = $item('.col1').text().trim() + + if (!/^\d{2}:\d{2}$/.test(time)) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.tbl > tbody > tr').toArray() +} diff --git a/sites/mediagenie.co.kr/mediagenie.co.kr.test.js b/sites/mediagenie.co.kr/mediagenie.co.kr.test.js index a50aa036..75004f2b 100644 --- a/sites/mediagenie.co.kr/mediagenie.co.kr.test.js +++ b/sites/mediagenie.co.kr/mediagenie.co.kr.test.js @@ -1,65 +1,65 @@ -// npm run grab -- --site=mediagenie.co.kr - -const { parser, url, request } = require('./mediagenie.co.kr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ENA_DRAMA', - xmltv_id: 'ENADRAMA.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://mediagenie.co.kr/ENA_DRAMA/?qd=20230125') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-24T15:20:00.000Z', - stop: '2023-01-24T16:34:00.000Z', - title: '대행사', - rating: { - system: 'KMRB', - value: '15' - } - }) - - expect(results[16]).toMatchObject({ - start: '2023-01-25T14:27:00.000Z', - stop: '2023-01-25T14:57:00.000Z', - title: '법쩐', - rating: { - system: 'KMRB', - value: '15' - } - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=mediagenie.co.kr + +const { parser, url, request } = require('./mediagenie.co.kr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ENA_DRAMA', + xmltv_id: 'ENADRAMA.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://mediagenie.co.kr/ENA_DRAMA/?qd=20230125') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + cookie: 'CUPID=d5ed6b77012aef2b4d4365ffd3a1a3a4' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-24T15:20:00.000Z', + stop: '2023-01-24T16:34:00.000Z', + title: '대행사', + rating: { + system: 'KMRB', + value: '15' + } + }) + + expect(results[16]).toMatchObject({ + start: '2023-01-25T14:27:00.000Z', + stop: '2023-01-25T14:57:00.000Z', + title: '법쩐', + rating: { + system: 'KMRB', + value: '15' + } + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/mediaklikk.hu/mediaklikk.hu.config.js b/sites/mediaklikk.hu/mediaklikk.hu.config.js index c525911a..26f41c64 100644 --- a/sites/mediaklikk.hu/mediaklikk.hu.config.js +++ b/sites/mediaklikk.hu/mediaklikk.hu.config.js @@ -1,85 +1,85 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mediaklikk.hu', - days: 2, - url: 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: function ({ date, channel }) { - const params = new URLSearchParams() - params.append('ChannelIds', `${channel.site_id},`) - params.append('Date', date.format('YYYY-MM-DD')) - - return params - } - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item) - let stop = parseStop($item) - if (!stop) stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item) { - const timeString = $item('*').data('from') - - return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') -} - -function parseStop($item) { - const timeString = $item('*').data('till') - if (!timeString || /^\+/.test(timeString)) return null - - try { - return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') - } catch (err) { - return null - } -} - -function parseTitle($item) { - return $item('.program_info > h1').text().trim() -} - -function parseDescription($item) { - return $item('.program_about > .program_description > p').text().trim() -} - -function parseIcon($item) { - const backgroundImage = $item('.program_about > .program_photo').css('background-image') - if (!backgroundImage) return null - const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] - if (!icon) return null - - return `https:${icon}` -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('li.program_body').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mediaklikk.hu', + days: 2, + url: 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: function ({ date, channel }) { + const params = new URLSearchParams() + params.append('ChannelIds', `${channel.site_id},`) + params.append('Date', date.format('YYYY-MM-DD')) + + return params + } + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item) + let stop = parseStop($item) + if (!stop) stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item) { + const timeString = $item('*').data('from') + + return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') +} + +function parseStop($item) { + const timeString = $item('*').data('till') + if (!timeString || /^\+/.test(timeString)) return null + + try { + return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') + } catch (err) { + return null + } +} + +function parseTitle($item) { + return $item('.program_info > h1').text().trim() +} + +function parseDescription($item) { + return $item('.program_about > .program_description > p').text().trim() +} + +function parseIcon($item) { + const backgroundImage = $item('.program_about > .program_photo').css('background-image') + if (!backgroundImage) return null + const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] + if (!icon) return null + + return `https:${icon}` +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('li.program_body').toArray() +} diff --git a/sites/mediaklikk.hu/mediaklikk.hu.test.js b/sites/mediaklikk.hu/mediaklikk.hu.test.js index 08b76e2c..b6e66b7e 100644 --- a/sites/mediaklikk.hu/mediaklikk.hu.test.js +++ b/sites/mediaklikk.hu/mediaklikk.hu.test.js @@ -1,73 +1,73 @@ -// npm run grab -- --site=mediaklikk.hu - -const { parser, url, request } = require('./mediaklikk.hu.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3', - xmltv_id: 'DuneTV.hu' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result.get('ChannelIds')).toBe('3,') - expect(result.get('Date')).toBe('2022-03-10') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-27T22:00:46.000Z', - stop: '2022-10-27T22:54:00.000Z', - title: 'A hegyi doktor - I. évad', - description: - 'Maxl iskolatársának, Vroninak az anyja egy autóbalesetben meghal. A 20 éves testvér, Vinzenz magához szeretné venni a lányt, ám a gyámüggyel problémái akadnak, ezért megpróbálja elszöktetni.(Eredeti hang digitálisan.)', - icon: 'https://mediaklikk.hu/wp-content/uploads/sites/4/2019/10/A-hegyi-doktor-I-évad-e1571318391226-150x150.jpg' - }) - - expect(results[56]).toMatchObject({ - start: '2022-10-28T20:35:05.000Z', - stop: '2022-10-28T21:05:05.000Z', - title: 'Szemtől szemben (1967)', - description: - 'Brad Fletcher bostoni történelemtanár, aki a délnyugati határvidéken kúrálja tüdőbetegségét, egy véletlen folytán összeakad Beauregard Bennett körözött útonállóval, akit végül maga segít a menekülésben. A tanárt lenyűgözi a törvényen kívüliek világa és felismeri, hogy értelmi felsőbbrendűségével bámulatosan tudja irányítani az embereket. Bennett csakhamar azt veszi észre, hogy a peremre szorult saját bandájában. Eközben a Pinkerton ügynökség beépített embere is csapdába igyekszik csalni mindnyájukat.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mediaklikk.hu + +const { parser, url, request } = require('./mediaklikk.hu.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3', + xmltv_id: 'DuneTV.hu' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://mediaklikk.hu/wp-content/plugins/hms-global-widgets/widgets/programGuide/programGuideInterface.php' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result.get('ChannelIds')).toBe('3,') + expect(result.get('Date')).toBe('2022-03-10') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-27T22:00:46.000Z', + stop: '2022-10-27T22:54:00.000Z', + title: 'A hegyi doktor - I. évad', + description: + 'Maxl iskolatársának, Vroninak az anyja egy autóbalesetben meghal. A 20 éves testvér, Vinzenz magához szeretné venni a lányt, ám a gyámüggyel problémái akadnak, ezért megpróbálja elszöktetni.(Eredeti hang digitálisan.)', + icon: 'https://mediaklikk.hu/wp-content/uploads/sites/4/2019/10/A-hegyi-doktor-I-évad-e1571318391226-150x150.jpg' + }) + + expect(results[56]).toMatchObject({ + start: '2022-10-28T20:35:05.000Z', + stop: '2022-10-28T21:05:05.000Z', + title: 'Szemtől szemben (1967)', + description: + 'Brad Fletcher bostoni történelemtanár, aki a délnyugati határvidéken kúrálja tüdőbetegségét, egy véletlen folytán összeakad Beauregard Bennett körözött útonállóval, akit végül maga segít a menekülésben. A tanárt lenyűgözi a törvényen kívüliek világa és felismeri, hogy értelmi felsőbbrendűségével bámulatosan tudja irányítani az embereket. Bennett csakhamar azt veszi észre, hogy a peremre szorult saját bandájában. Eközben a Pinkerton ügynökség beépített embere is csapdába igyekszik csalni mindnyájukat.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mediaset.it/mediaset.it.config.js b/sites/mediaset.it/mediaset.it.config.js index 40a84876..a5c0a82c 100644 --- a/sites/mediaset.it/mediaset.it.config.js +++ b/sites/mediaset.it/mediaset.it.config.js @@ -1,49 +1,49 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mediaset.it', - days: 2, - url: function ({ date, channel }) { - return `http://www.mediaset.it/guidatv/inc/canali/${date.format('YYYYMM')}/${date.format( - 'YYYYMMDD' - )}_${channel.site_id}.sjson` - }, - parser: function ({ content, date }) { - const programs = [] - const data = JSON.parse(content) - if (!data.events) return programs - - data.events.forEach(item => { - if (item.title && item.startTime && item.endTime) { - const start = dayjs - .utc(item.startTime, 'HH:mm') - .set('D', date.get('D')) - .set('M', date.get('M')) - .set('y', date.get('y')) - .toString() - - const stop = dayjs - .utc(item.endTime, 'HH:mm') - .set('D', date.get('D')) - .set('M', date.get('M')) - .set('y', date.get('y')) - .toString() - - programs.push({ - title: item.displayTitle || item.title, - description: item.description, - category: item.genere, - start, - stop - }) - } - }) - - return programs - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mediaset.it', + days: 2, + url: function ({ date, channel }) { + return `http://www.mediaset.it/guidatv/inc/canali/${date.format('YYYYMM')}/${date.format( + 'YYYYMMDD' + )}_${channel.site_id}.sjson` + }, + parser: function ({ content, date }) { + const programs = [] + const data = JSON.parse(content) + if (!data.events) return programs + + data.events.forEach(item => { + if (item.title && item.startTime && item.endTime) { + const start = dayjs + .utc(item.startTime, 'HH:mm') + .set('D', date.get('D')) + .set('M', date.get('M')) + .set('y', date.get('y')) + .toString() + + const stop = dayjs + .utc(item.endTime, 'HH:mm') + .set('D', date.get('D')) + .set('M', date.get('M')) + .set('y', date.get('y')) + .toString() + + programs.push({ + title: item.displayTitle || item.title, + description: item.description, + category: item.genere, + start, + stop + }) + } + }) + + return programs + } +} diff --git a/sites/melita.com/melita.com.config.js b/sites/melita.com/melita.com.config.js index 7fff861a..0bd89831 100644 --- a/sites/melita.com/melita.com.config.js +++ b/sites/melita.com/melita.com.config.js @@ -1,88 +1,88 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'melita.com', - days: 2, - url: function ({ channel, date }) { - return `https://androme.melitacable.com/api/epg/v1/schedule/channel/${ - channel.site_id - }/from/${date.format('YYYY-MM-DDTHH:mmZ')}/until/${date - .add(1, 'd') - .format('YYYY-MM-DDTHH:mmZ')}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.shortSynopsis, - icon: parseIcon(item), - category: item.tags, - season: item.season, - episode: item.episode, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://androme.melitacable.com/api/epg/v2/channel') - .then(r => r.data) - .catch(console.log) - - return channels - .filter(i => !i.audioOnly && i.enabled) - .map(i => { - return { - name: i.name, - site_id: i.id - } - }) - } -} - -function parseStart(item) { - if (!item.published || !item.published.start) return null - - return dayjs(item.published.start) -} - -function parseStop(item) { - if (!item.published || !item.published.end) return null - - return dayjs(item.published.end) -} - -function parseIcon(item) { - return item.posterImage ? item.posterImage + '?form=epg-card-6' : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if ( - !data || - !data.schedules || - !data.programs || - !data.seasons || - !data.series || - !Array.isArray(data.schedules) - ) - return [] - - return data.schedules - .map(i => { - const program = data.programs.find(p => p.id === i.program) || {} - if (!program.season) return null - const season = data.seasons.find(s => s.id === program.season) || {} - if (!season.series) return null - const series = data.series.find(s => s.id === season.series) - - return { ...i, ...program, ...season, ...series } - }) - .filter(i => i) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'melita.com', + days: 2, + url: function ({ channel, date }) { + return `https://androme.melitacable.com/api/epg/v1/schedule/channel/${ + channel.site_id + }/from/${date.format('YYYY-MM-DDTHH:mmZ')}/until/${date + .add(1, 'd') + .format('YYYY-MM-DDTHH:mmZ')}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.shortSynopsis, + icon: parseIcon(item), + category: item.tags, + season: item.season, + episode: item.episode, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://androme.melitacable.com/api/epg/v2/channel') + .then(r => r.data) + .catch(console.log) + + return channels + .filter(i => !i.audioOnly && i.enabled) + .map(i => { + return { + name: i.name, + site_id: i.id + } + }) + } +} + +function parseStart(item) { + if (!item.published || !item.published.start) return null + + return dayjs(item.published.start) +} + +function parseStop(item) { + if (!item.published || !item.published.end) return null + + return dayjs(item.published.end) +} + +function parseIcon(item) { + return item.posterImage ? item.posterImage + '?form=epg-card-6' : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if ( + !data || + !data.schedules || + !data.programs || + !data.seasons || + !data.series || + !Array.isArray(data.schedules) + ) + return [] + + return data.schedules + .map(i => { + const program = data.programs.find(p => p.id === i.program) || {} + if (!program.season) return null + const season = data.seasons.find(s => s.id === program.season) || {} + if (!season.series) return null + const series = data.series.find(s => s.id === season.series) + + return { ...i, ...program, ...season, ...series } + }) + .filter(i => i) +} diff --git a/sites/melita.com/melita.com.test.js b/sites/melita.com/melita.com.test.js index 3fa48f75..d126668a 100644 --- a/sites/melita.com/melita.com.test.js +++ b/sites/melita.com/melita.com.test.js @@ -1,52 +1,52 @@ -// npm run channels:parse -- --config=./sites/melita.com/melita.com.config.js --output=./sites/melita.com/melita.com.channels.xml -// npm run grab -- --site=melita.com - -const { parser, url } = require('./melita.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-04-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995', - xmltv_id: 'TVM.mt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00' - ) -}) - -it('can parse response', () => { - const content = - '{"schedules":[{"id":"138dabff-131a-42a0-9373-203545933dd0","published":{"start":"2022-04-20T06:25:00Z","end":"2022-04-20T06:45:00Z"},"program":"ae52299a-3c99-4d34-9932-e21d383f9800","live":false,"blackouts":[]}],"programs":[{"id":"ae52299a-3c99-4d34-9932-e21d383f9800","title":"How I Met Your Mother","shortSynopsis":"Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.","posterImage":"https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg","episode":12,"episodeTitle":"Symphony of Illumination","season":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","genres":["3.5.7.3"],"tags":["comedy"],"adult":false}],"seasons":[{"id":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","title":"How I Met Your Mother","adult":false,"season":7,"series":"858c535a-abbb-451b-807a-94196997ea2d"}],"series":[{"id":"858c535a-abbb-451b-807a-94196997ea2d","title":"How I Met Your Mother","adult":false}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-20T06:25:00.000Z', - stop: '2022-04-20T06:45:00.000Z', - title: 'How I Met Your Mother', - description: - 'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.', - season: 7, - episode: 12, - icon: 'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6', - category: ['comedy'] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/melita.com/melita.com.config.js --output=./sites/melita.com/melita.com.channels.xml +// npm run grab -- --site=melita.com + +const { parser, url } = require('./melita.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-04-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '4d40a9f9-12fd-4f03-8072-61c637ff6995', + xmltv_id: 'TVM.mt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://androme.melitacable.com/api/epg/v1/schedule/channel/4d40a9f9-12fd-4f03-8072-61c637ff6995/from/2022-04-20T00:00+00:00/until/2022-04-21T00:00+00:00' + ) +}) + +it('can parse response', () => { + const content = + '{"schedules":[{"id":"138dabff-131a-42a0-9373-203545933dd0","published":{"start":"2022-04-20T06:25:00Z","end":"2022-04-20T06:45:00Z"},"program":"ae52299a-3c99-4d34-9932-e21d383f9800","live":false,"blackouts":[]}],"programs":[{"id":"ae52299a-3c99-4d34-9932-e21d383f9800","title":"How I Met Your Mother","shortSynopsis":"Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.","posterImage":"https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg","episode":12,"episodeTitle":"Symphony of Illumination","season":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","genres":["3.5.7.3"],"tags":["comedy"],"adult":false}],"seasons":[{"id":"fdd6e42c-97f9-4d7a-aaca-78b53378f960","title":"How I Met Your Mother","adult":false,"season":7,"series":"858c535a-abbb-451b-807a-94196997ea2d"}],"series":[{"id":"858c535a-abbb-451b-807a-94196997ea2d","title":"How I Met Your Mother","adult":false}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-20T06:25:00.000Z', + stop: '2022-04-20T06:45:00.000Z', + title: 'How I Met Your Mother', + description: + 'Symphony of Illumination - Robin gets some bad news and decides to keep it to herself. Marshall decorates the house.', + season: 7, + episode: 12, + icon: 'https://androme.melitacable.com/media/images/epg/bc/07/p8953134_e_h10_ad.jpg?form=epg-card-6', + category: ['comedy'] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/meo.pt/meo.pt.config.js b/sites/meo.pt/meo.pt.config.js index baa87c55..1e95c4ee 100644 --- a/sites/meo.pt/meo.pt.config.js +++ b/sites/meo.pt/meo.pt.config.js @@ -1,60 +1,60 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'meo.pt', - days: 2, - url: 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels', - request: { - method: 'POST', - headers: { - Origin: 'https://www.meo.pt' - }, - data: function ({ channel, date }) { - return { - service: 'channelsguide', - channels: [channel.site_id], - dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'), - dateEnd: date.add(1, 'd').format('YYYY-MM-DDT00:00:00-00:00'), - accountID: '' - } - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - let stop = parseStop(item) - if (stop < start) { - stop = stop.plus({ days: 1 }) - } - programs.push({ - title: item.name, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - return DateTime.fromFormat(`${item.date} ${item.timeIni}`, 'd-M-yyyy HH:mm', { - zone: 'Europe/Lisbon' - }).toUTC() -} - -function parseStop(item) { - return DateTime.fromFormat(`${item.date} ${item.timeEnd}`, 'd-M-yyyy HH:mm', { - zone: 'Europe/Lisbon' - }).toUTC() -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - const programs = data?.d?.channels?.[0]?.programs - - return Array.isArray(programs) ? programs : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'meo.pt', + days: 2, + url: 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels', + request: { + method: 'POST', + headers: { + Origin: 'https://www.meo.pt' + }, + data: function ({ channel, date }) { + return { + service: 'channelsguide', + channels: [channel.site_id], + dateStart: date.format('YYYY-MM-DDT00:00:00-00:00'), + dateEnd: date.add(1, 'd').format('YYYY-MM-DDT00:00:00-00:00'), + accountID: '' + } + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + let stop = parseStop(item) + if (stop < start) { + stop = stop.plus({ days: 1 }) + } + programs.push({ + title: item.name, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + return DateTime.fromFormat(`${item.date} ${item.timeIni}`, 'd-M-yyyy HH:mm', { + zone: 'Europe/Lisbon' + }).toUTC() +} + +function parseStop(item) { + return DateTime.fromFormat(`${item.date} ${item.timeEnd}`, 'd-M-yyyy HH:mm', { + zone: 'Europe/Lisbon' + }).toUTC() +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + const programs = data?.d?.channels?.[0]?.programs + + return Array.isArray(programs) ? programs : [] +} diff --git a/sites/meo.pt/meo.pt.test.js b/sites/meo.pt/meo.pt.test.js index e4fe3ccb..7fb4019b 100644 --- a/sites/meo.pt/meo.pt.test.js +++ b/sites/meo.pt/meo.pt.test.js @@ -1,62 +1,62 @@ -// npm run grab -- --site=meo.pt - -const { parser, url, request } = require('./meo.pt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'RTPM', - xmltv_id: 'RTPMadeira.pt' -} - -it('can generate valid url', () => { - expect(url).toBe( - 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels' - ) -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Origin: 'https://www.meo.pt' - }) -}) - -it('can generate valid request method', () => { - expect(request.data({ channel, date })).toMatchObject({ - service: 'channelsguide', - channels: ['RTPM'], - dateStart: '2022-12-02T00:00:00-00:00', - dateEnd: '2022-12-03T00:00:00-00:00', - accountID: '' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T23:35:00.000Z', - stop: '2022-12-02T00:17:00.000Z', - title: 'Walker, O Ranger Do Texas T6 - Ep. 14' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=meo.pt + +const { parser, url, request } = require('./meo.pt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'RTPM', + xmltv_id: 'RTPMadeira.pt' +} + +it('can generate valid url', () => { + expect(url).toBe( + 'https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getProgramsFromChannels' + ) +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Origin: 'https://www.meo.pt' + }) +}) + +it('can generate valid request method', () => { + expect(request.data({ channel, date })).toMatchObject({ + service: 'channelsguide', + channels: ['RTPM'], + dateStart: '2022-12-02T00:00:00-00:00', + dateEnd: '2022-12-03T00:00:00-00:00', + accountID: '' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T23:35:00.000Z', + stop: '2022-12-02T00:17:00.000Z', + title: 'Walker, O Ranger Do Texas T6 - Ep. 14' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mewatch.sg/mewatch.sg.config.js b/sites/mewatch.sg/mewatch.sg.config.js index baf58b9a..bb029050 100644 --- a/sites/mewatch.sg/mewatch.sg.config.js +++ b/sites/mewatch.sg/mewatch.sg.config.js @@ -1,61 +1,61 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'mewatch.sg', - days: 2, - url: function ({ channel, date }) { - return `https://cdn.mewatch.sg/api/schedules?channels=${channel.site_id}&date=${date.format( - 'YYYY-MM-DD' - )}&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const info = item.item - programs.push({ - title: info.title, - description: info.description, - icon: info.images.tile, - episode: info.episodeNumber, - season: info.seasonNumber, - start: parseStart(item), - stop: parseStop(item), - rating: parseRating(info) - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs(item.startDate) -} - -function parseStop(item) { - return dayjs(item.endDate) -} - -function parseRating(info) { - const classification = info.classification - if (classification && classification.code) { - const [, system, value] = classification.code.match(/^([A-Z]+)-([A-Z0-9]+)/) || [ - null, - null, - null - ] - - return { system, value } - } - - return null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data)) return [] - const channelData = data.find(i => i.channelId === channel.site_id) - - return channelData && Array.isArray(channelData.schedules) ? channelData.schedules : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'mewatch.sg', + days: 2, + url: function ({ channel, date }) { + return `https://cdn.mewatch.sg/api/schedules?channels=${channel.site_id}&date=${date.format( + 'YYYY-MM-DD' + )}&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const info = item.item + programs.push({ + title: info.title, + description: info.description, + icon: info.images.tile, + episode: info.episodeNumber, + season: info.seasonNumber, + start: parseStart(item), + stop: parseStop(item), + rating: parseRating(info) + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs(item.startDate) +} + +function parseStop(item) { + return dayjs(item.endDate) +} + +function parseRating(info) { + const classification = info.classification + if (classification && classification.code) { + const [, system, value] = classification.code.match(/^([A-Z]+)-([A-Z0-9]+)/) || [ + null, + null, + null + ] + + return { system, value } + } + + return null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data)) return [] + const channelData = data.find(i => i.channelId === channel.site_id) + + return channelData && Array.isArray(channelData.schedules) ? channelData.schedules : [] +} diff --git a/sites/mewatch.sg/mewatch.sg.test.js b/sites/mewatch.sg/mewatch.sg.test.js index ca944c90..fdc6f9c3 100644 --- a/sites/mewatch.sg/mewatch.sg.test.js +++ b/sites/mewatch.sg/mewatch.sg.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=mewatch.sg - -const { parser, url } = require('./mewatch.sg.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-06-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '97098', - xmltv_id: 'Channel5Singapore.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-11&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all' - ) -}) - -it('can parse response', () => { - const content = - '[{"channelId":"97098","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[{"channelId":"97098","customId":"37040748","endDate":"2022-06-11T21:30:00Z","id":"788a7dd","live":false,"startDate":"2022-06-11T21:00:00Z","isGap":false,"InteractiveType":"0","item":{"type":"episode","title":"Open Homes S3 - EP 2","blackoutMessage":"Programme is not available for live streaming.","description":"Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.","classification":{"code":"IMDA-G (Violence)","name":"G (Violence)"},"episodeNumber":2,"episodeTitle":"Collaroy, Sydney","seasonNumber":3,"images":{"wallpaper":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853691\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all","tile":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853697\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all"},"enableCatchUp":true,"enableStartOver":false,"enableSeeking":false,"programSource":"ACQUIRED","simulcast":"LOCAL","masterReferenceKey":"0CH50CH5A0105567800020A0000000000P3254400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}]}]' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-11T21:00:00.000Z', - stop: '2022-06-11T21:30:00.000Z', - title: 'Open Homes S3 - EP 2', - description: - 'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.', - icon: "https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all", - episode: 2, - season: 3, - rating: { - system: 'IMDA', - value: 'G' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '[{"channelId":"9798","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[]}]', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mewatch.sg + +const { parser, url } = require('./mewatch.sg.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-06-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '97098', + xmltv_id: 'Channel5Singapore.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://cdn.mewatch.sg/api/schedules?channels=97098&date=2022-06-11&duration=24&ff=idp,ldp,rpt,cd&hour=21&intersect=true&lang=en&segments=all' + ) +}) + +it('can parse response', () => { + const content = + '[{"channelId":"97098","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[{"channelId":"97098","customId":"37040748","endDate":"2022-06-11T21:30:00Z","id":"788a7dd","live":false,"startDate":"2022-06-11T21:00:00Z","isGap":false,"InteractiveType":"0","item":{"type":"episode","title":"Open Homes S3 - EP 2","blackoutMessage":"Programme is not available for live streaming.","description":"Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.","classification":{"code":"IMDA-G (Violence)","name":"G (Violence)"},"episodeNumber":2,"episodeTitle":"Collaroy, Sydney","seasonNumber":3,"images":{"wallpaper":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853691\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all","tile":"https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'4853697\'&EntityType=\'LinearSchedule\'&EntityId=\'788a7dd9-9b12-446f-91b4-c8ac9fec95e5\'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all"},"enableCatchUp":true,"enableStartOver":false,"enableSeeking":false,"programSource":"ACQUIRED","simulcast":"LOCAL","masterReferenceKey":"0CH50CH5A0105567800020A0000000000P3254400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}]}]' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-11T21:00:00.000Z', + stop: '2022-06-11T21:30:00.000Z', + title: 'Open Homes S3 - EP 2', + description: + 'Mike heads down to the Sydney beaches to visit a beachside renovation with all the bells and whistles, we see a kitchen tip and recipe anyone can do at home. We finish up in the prestigious Byron bay to visit a multi million dollar award winning home.', + icon: "https://production.togglestatic.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='4853697'&EntityType='LinearSchedule'&EntityId='788a7dd9-9b12-446f-91b4-c8ac9fec95e5'&Width=1280&Height=720&device=web_browser&subscriptions=Anonymous&segmentationTags=all", + episode: 2, + season: 3, + rating: { + system: 'IMDA', + value: 'G' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '[{"channelId":"9798","startDate":"2022-06-11T21:00:00.000Z","endDate":"2022-06-12T21:00:00.000Z","schedules":[]}]', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mi.tv/mi.tv.config.js b/sites/mi.tv/mi.tv.config.js index 946d10e1..1c46f095 100644 --- a/sites/mi.tv/mi.tv.config.js +++ b/sites/mi.tv/mi.tv.config.js @@ -1,78 +1,78 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mi.tv', - days: 2, - url({ date, channel }) { - const [country, id] = channel.site_id.split('#') - - return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('a > div.content > span.time').text() - if (!timeString) return null - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm') -} - -function parseTitle($item) { - return $item('a > div.content > h2').text().trim() -} - -function parseCategory($item) { - return $item('a > div.content > span.sub-title').text().trim() -} - -function parseDescription($item) { - return $item('a > div.content > p.synopsis').text().trim() -} - -function parseIcon($item) { - const backgroundImage = $item('a > div.image-parent > div.image').css('background-image') - const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] - - return icon -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#listings > ul > li').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mi.tv', + days: 2, + url({ date, channel }) { + const [country, id] = channel.site_id.split('#') + + return `https://mi.tv/${country}/async/channel/${id}/${date.format('YYYY-MM-DD')}/0` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('a > div.content > span.time').text() + if (!timeString) return null + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return dayjs.utc(dateString, 'MM/DD/YYYY HH:mm') +} + +function parseTitle($item) { + return $item('a > div.content > h2').text().trim() +} + +function parseCategory($item) { + return $item('a > div.content > span.sub-title').text().trim() +} + +function parseDescription($item) { + return $item('a > div.content > p.synopsis').text().trim() +} + +function parseIcon($item) { + const backgroundImage = $item('a > div.image-parent > div.image').css('background-image') + const [, icon] = backgroundImage.match(/url\('(.*)'\)/) || [null, null] + + return icon +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#listings > ul > li').toArray() +} diff --git a/sites/mi.tv/mi.tv.test.js b/sites/mi.tv/mi.tv.test.js index 5a4a1be0..079ef682 100644 --- a/sites/mi.tv/mi.tv.test.js +++ b/sites/mi.tv/mi.tv.test.js @@ -1,68 +1,68 @@ -// npm run grab -- --site=mi.tv - -const { parser, url } = require('./mi.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ar#24-7-canal-de-noticias', - xmltv_id: '247CanaldeNoticias.ar' -} -const content = - '' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0' - ) -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T03:00:00.000Z', - stop: '2021-11-24T23:00:00.000Z', - title: 'Trasnoche de 24/7', - category: 'Interés general', - description: 'Lo más visto de la semana en nuestra pantalla.', - icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - }, - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T01:00:00.000Z', - title: 'Noticiero central - Segunda edición', - category: 'Noticiero', - description: - 'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.', - icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - }, - { - start: '2021-11-25T01:00:00.000Z', - stop: '2021-11-25T02:00:00.000Z', - title: 'Plus energético', - category: 'Cultural', - description: - 'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.', - icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mi.tv + +const { parser, url } = require('./mi.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ar#24-7-canal-de-noticias', + xmltv_id: '247CanaldeNoticias.ar' +} +const content = + '' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://mi.tv/ar/async/channel/24-7-canal-de-noticias/2021-11-24/0' + ) +}) + +it('can parse response', () => { + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T03:00:00.000Z', + stop: '2021-11-24T23:00:00.000Z', + title: 'Trasnoche de 24/7', + category: 'Interés general', + description: 'Lo más visto de la semana en nuestra pantalla.', + icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + }, + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T01:00:00.000Z', + title: 'Noticiero central - Segunda edición', + category: 'Noticiero', + description: + 'Cerramos el día con un completo resumen de los temas más relevantes con columnistas y análisis especiales para terminar el día.', + icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + }, + { + start: '2021-11-25T01:00:00.000Z', + stop: '2021-11-25T02:00:00.000Z', + title: 'Plus energético', + category: 'Cultural', + description: + 'La energía tiene mucho para mostrar. Este programa reúne a las principales empresas y protagonistas de la actividad que esta revolucionando la región.', + icon: 'https://cdn.mitvstatic.com/programs/fallback_other_l_m.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mncvision.id/mncvision.id.config.js b/sites/mncvision.id/mncvision.id.config.js index 1b5949f4..25e1e90f 100644 --- a/sites/mncvision.id/mncvision.id.config.js +++ b/sites/mncvision.id/mncvision.id.config.js @@ -1,184 +1,184 @@ -const _ = require('lodash') -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mncvision.id', - days: 2, - url: 'https://mncvision.id/schedule/table', - request: { - method: 'POST', - data: function ({ channel, date }) { - const formData = new URLSearchParams() - formData.append('search_model', 'channel') - formData.append('af0rmelement', 'aformelement') - formData.append('fdate', date.format('YYYY-MM-DD')) - formData.append('fchannel', channel.site_id) - formData.append('submit', 'Search') - - return formData - }, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - jar: null - }, - async parser({ content, date, headers, channel }) { - const programs = [] - const cookies = parseCookies(headers) - if (!cookies) return programs - let items = parseItems(content) - if (!items.length) return programs - - const pages = parsePages(content) - for (let url of pages) { - items = items.concat(parseItems(await loadNextPage(url, cookies))) - } - - const langCookies = await loadLangCookies(channel) - if (!langCookies) return programs - - for (const item of items) { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - const description = await loadDescription($item, langCookies) - programs.push({ - title: parseTitle($item), - season: parseSeason($item), - episode: parseEpisode($item), - description, - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://www.mncvision.id/schedule') - .then(response => response.data) - .catch(console.error) - - const $ = cheerio.load(data) - const items = $('select[name="fchannel"] option').toArray() - const channels = items.map(item => { - const $item = cheerio.load(item) - - return { - lang: 'id', - site_id: $item('*').attr('value'), - name: $item('*').text() - } - }) - - return channels - } -} - -function parseSeason($item) { - const title = parseTitle($item) - const [, season] = title.match(/ S(\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - const title = parseTitle($item) - const [, episode] = title.match(/ Ep (\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseDuration($item) { - let duration = $item('td:nth-child(3)').text() - const match = duration.match(/(\d{2}):(\d{2})/) - const hours = parseInt(match[1]) - const minutes = parseInt(match[2]) - - return hours * 60 + minutes -} - -function parseStart($item, date) { - let time = $item('td:nth-child(1)').text() - time = `${date.format('DD/MM/YYYY')} ${time}` - - return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') -} - -function parseTitle($item) { - return $item('td:nth-child(2) > a').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('tr[valign="top"]').toArray() -} - -function parsePages(content) { - const $ = cheerio.load(content) - const links = $('#schedule > div.schedule_search_result_container > div.box.well > a') - .map((i, el) => { - return $(el).attr('href') - }) - .get() - - return _.uniq(links) -} - -function loadNextPage(url, cookies) { - return axios - .get(url, { headers: { Cookie: cookies }, timeout: 30000 }) - .then(r => r.data) - .catch(err => { - console.log(err.message) - - return null - }) -} - -function loadLangCookies(channel) { - const languages = { - en: 'english', - id: 'indonesia' - } - const url = `https://www.mncvision.id/language_switcher/setlang/${languages[channel.lang]}/` - - return axios - .get(url, { timeout: 30000 }) - .then(r => parseCookies(r.headers)) - .catch(error => console.log(error.message)) -} - -async function loadDescription($item, cookies) { - const url = $item('a').attr('href') - if (!url) return null - const content = await axios - .get(url, { - headers: { 'X-Requested-With': 'XMLHttpRequest', Cookie: cookies }, - timeout: 30000 - }) - .then(r => r.data) - .catch(error => console.log(error.message)) - if (!content) return null - - const $page = cheerio.load(content) - const description = $page('.synopsis').text().trim() - - return description !== '-' ? description : null -} - -function parseCookies(headers) { - return Array.isArray(headers['set-cookie']) ? headers['set-cookie'].join(';') : null -} +const _ = require('lodash') +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mncvision.id', + days: 2, + url: 'https://mncvision.id/schedule/table', + request: { + method: 'POST', + data: function ({ channel, date }) { + const formData = new URLSearchParams() + formData.append('search_model', 'channel') + formData.append('af0rmelement', 'aformelement') + formData.append('fdate', date.format('YYYY-MM-DD')) + formData.append('fchannel', channel.site_id) + formData.append('submit', 'Search') + + return formData + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + jar: null + }, + async parser({ content, date, headers, channel }) { + const programs = [] + const cookies = parseCookies(headers) + if (!cookies) return programs + let items = parseItems(content) + if (!items.length) return programs + + const pages = parsePages(content) + for (let url of pages) { + items = items.concat(parseItems(await loadNextPage(url, cookies))) + } + + const langCookies = await loadLangCookies(channel) + if (!langCookies) return programs + + for (const item of items) { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + const description = await loadDescription($item, langCookies) + programs.push({ + title: parseTitle($item), + season: parseSeason($item), + episode: parseEpisode($item), + description, + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://www.mncvision.id/schedule') + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(data) + const items = $('select[name="fchannel"] option').toArray() + const channels = items.map(item => { + const $item = cheerio.load(item) + + return { + lang: 'id', + site_id: $item('*').attr('value'), + name: $item('*').text() + } + }) + + return channels + } +} + +function parseSeason($item) { + const title = parseTitle($item) + const [, season] = title.match(/ S(\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + const title = parseTitle($item) + const [, episode] = title.match(/ Ep (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseDuration($item) { + let duration = $item('td:nth-child(3)').text() + const match = duration.match(/(\d{2}):(\d{2})/) + const hours = parseInt(match[1]) + const minutes = parseInt(match[2]) + + return hours * 60 + minutes +} + +function parseStart($item, date) { + let time = $item('td:nth-child(1)').text() + time = `${date.format('DD/MM/YYYY')} ${time}` + + return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') +} + +function parseTitle($item) { + return $item('td:nth-child(2) > a').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('tr[valign="top"]').toArray() +} + +function parsePages(content) { + const $ = cheerio.load(content) + const links = $('#schedule > div.schedule_search_result_container > div.box.well > a') + .map((i, el) => { + return $(el).attr('href') + }) + .get() + + return _.uniq(links) +} + +function loadNextPage(url, cookies) { + return axios + .get(url, { headers: { Cookie: cookies }, timeout: 30000 }) + .then(r => r.data) + .catch(err => { + console.log(err.message) + + return null + }) +} + +function loadLangCookies(channel) { + const languages = { + en: 'english', + id: 'indonesia' + } + const url = `https://www.mncvision.id/language_switcher/setlang/${languages[channel.lang]}/` + + return axios + .get(url, { timeout: 30000 }) + .then(r => parseCookies(r.headers)) + .catch(error => console.log(error.message)) +} + +async function loadDescription($item, cookies) { + const url = $item('a').attr('href') + if (!url) return null + const content = await axios + .get(url, { + headers: { 'X-Requested-With': 'XMLHttpRequest', Cookie: cookies }, + timeout: 30000 + }) + .then(r => r.data) + .catch(error => console.log(error.message)) + if (!content) return null + + const $page = cheerio.load(content) + const description = $page('.synopsis').text().trim() + + return description !== '-' ? description : null +} + +function parseCookies(headers) { + return Array.isArray(headers['set-cookie']) ? headers['set-cookie'].join(';') : null +} diff --git a/sites/mncvision.id/mncvision.id.test.js b/sites/mncvision.id/mncvision.id.test.js index 81995497..712df202 100644 --- a/sites/mncvision.id/mncvision.id.test.js +++ b/sites/mncvision.id/mncvision.id.test.js @@ -1,150 +1,150 @@ -// npm run channels:parse -- --config=./sites/mncvision.id/mncvision.id.config.js --output=./sites/mncvision.id/mncvision.id.channels.xml -// npm run grab -- --site=mncvision.id - -const { parser, url, request } = require('./mncvision.id.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-05', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '38', - xmltv_id: 'MiaoMi.hk', - lang: 'id' -} -const headers = { - 'set-cookie': [ - 's1nd0vL=05e9pr6gi112tdmutsn7big93o75r0b0; expires=Wed, 05-Oct-2022 14:18:22 GMT; Max-Age=7200; path=/; HttpOnly' - ] -} - -it('can generate valid url', () => { - expect(url).toBe('https://mncvision.id/schedule/table') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ channel, date }) - expect(data.get('search_model')).toBe('channel') - expect(data.get('af0rmelement')).toBe('aformelement') - expect(data.get('fdate')).toBe('2022-10-05') - expect(data.get('fchannel')).toBe('38') - expect(data.get('submit')).toBe('Search') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const indonesiaHeaders = { - 'set-cookie': [ - 's1nd0vL=e3vjb0oaf9vijiqsg7cml4i7fdkq16db; expires=Wed, 05-Oct-2022 14:54:16 GMT; Max-Age=7200; path=/; HttpOnly' - ] - } - const englishHeaders = { - 'set-cookie': [ - 's1nd0vL=hfd6hpnpr6gvgart0d8rf7ef6t4gi7nr; expires=Wed, 05-Oct-2022 15:08:55 GMT; Max-Age=7200; path=/; HttpOnly' - ] - } - axios.get.mockImplementation((url, opts) => { - if ( - url === 'https://www.mncvision.id/schedule/table/startno/50' && - opts.headers['Cookie'] === headers['set-cookie'][0] - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_p2.html')) - }) - } else if (url === 'https://www.mncvision.id/language_switcher/setlang/indonesia/') { - return Promise.resolve({ - headers: indonesiaHeaders - }) - } else if (url === 'https://www.mncvision.id/language_switcher/setlang/english/') { - return Promise.resolve({ - headers: englishHeaders - }) - } else if ( - url === - 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && - opts.headers['Cookie'] === indonesiaHeaders['set-cookie'][0] - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_id.html')) - }) - } else if ( - url === - 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && - opts.headers['Cookie'] === englishHeaders['set-cookie'][0] - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) - }) - } - - return Promise.resolve({ data: '' }) - }) - - let indonesiaResults = await parser({ date, content, channel, headers }) - indonesiaResults = indonesiaResults.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(indonesiaResults[0]).toMatchObject({ - start: '2022-10-04T17:00:00.000Z', - stop: '2022-10-04T17:06:00.000Z', - title: 'Adventures With Miao Mi, Ep 1', - episode: 1, - description: - 'Ketika anak-anak mulai menghilang, sekelompok anak kecil harus menghadapi ketakutan terbesar mereka ketika mereka melawan sesosok badut pembunuh yang jahat.' - }) - - expect(indonesiaResults[4]).toMatchObject({ - start: '2022-10-04T17:33:00.000Z', - stop: '2022-10-04T17:46:00.000Z', - title: 'Leo Wildlife Ranger S2, Ep 27', - season: 2, - episode: 27 - }) - - let englishResults = await parser({ date, content, channel: { ...channel, lang: 'en' }, headers }) - englishResults = englishResults.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(englishResults[0]).toMatchObject({ - start: '2022-10-04T17:00:00.000Z', - stop: '2022-10-04T17:06:00.000Z', - title: 'Adventures With Miao Mi, Ep 1', - episode: 1, - description: - 'When children begin to disappear, a group of young kids have to face their biggest fears when they square off against a murderous, evil clown.' - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - let results = await parser({ - date, - channel, - content, - headers - }) - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/mncvision.id/mncvision.id.config.js --output=./sites/mncvision.id/mncvision.id.channels.xml +// npm run grab -- --site=mncvision.id + +const { parser, url, request } = require('./mncvision.id.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-05', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '38', + xmltv_id: 'MiaoMi.hk', + lang: 'id' +} +const headers = { + 'set-cookie': [ + 's1nd0vL=05e9pr6gi112tdmutsn7big93o75r0b0; expires=Wed, 05-Oct-2022 14:18:22 GMT; Max-Age=7200; path=/; HttpOnly' + ] +} + +it('can generate valid url', () => { + expect(url).toBe('https://mncvision.id/schedule/table') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ channel, date }) + expect(data.get('search_model')).toBe('channel') + expect(data.get('af0rmelement')).toBe('aformelement') + expect(data.get('fdate')).toBe('2022-10-05') + expect(data.get('fchannel')).toBe('38') + expect(data.get('submit')).toBe('Search') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const indonesiaHeaders = { + 'set-cookie': [ + 's1nd0vL=e3vjb0oaf9vijiqsg7cml4i7fdkq16db; expires=Wed, 05-Oct-2022 14:54:16 GMT; Max-Age=7200; path=/; HttpOnly' + ] + } + const englishHeaders = { + 'set-cookie': [ + 's1nd0vL=hfd6hpnpr6gvgart0d8rf7ef6t4gi7nr; expires=Wed, 05-Oct-2022 15:08:55 GMT; Max-Age=7200; path=/; HttpOnly' + ] + } + axios.get.mockImplementation((url, opts) => { + if ( + url === 'https://www.mncvision.id/schedule/table/startno/50' && + opts.headers['Cookie'] === headers['set-cookie'][0] + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_p2.html')) + }) + } else if (url === 'https://www.mncvision.id/language_switcher/setlang/indonesia/') { + return Promise.resolve({ + headers: indonesiaHeaders + }) + } else if (url === 'https://www.mncvision.id/language_switcher/setlang/english/') { + return Promise.resolve({ + headers: englishHeaders + }) + } else if ( + url === + 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && + opts.headers['Cookie'] === indonesiaHeaders['set-cookie'][0] + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_id.html')) + }) + } else if ( + url === + 'https://mncvision.id/schedule/detail/2022100500000038/Adventures-With-Miao-Mi-Ep-1/1' && + opts.headers['Cookie'] === englishHeaders['set-cookie'][0] + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) + }) + } + + return Promise.resolve({ data: '' }) + }) + + let indonesiaResults = await parser({ date, content, channel, headers }) + indonesiaResults = indonesiaResults.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(indonesiaResults[0]).toMatchObject({ + start: '2022-10-04T17:00:00.000Z', + stop: '2022-10-04T17:06:00.000Z', + title: 'Adventures With Miao Mi, Ep 1', + episode: 1, + description: + 'Ketika anak-anak mulai menghilang, sekelompok anak kecil harus menghadapi ketakutan terbesar mereka ketika mereka melawan sesosok badut pembunuh yang jahat.' + }) + + expect(indonesiaResults[4]).toMatchObject({ + start: '2022-10-04T17:33:00.000Z', + stop: '2022-10-04T17:46:00.000Z', + title: 'Leo Wildlife Ranger S2, Ep 27', + season: 2, + episode: 27 + }) + + let englishResults = await parser({ date, content, channel: { ...channel, lang: 'en' }, headers }) + englishResults = englishResults.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(englishResults[0]).toMatchObject({ + start: '2022-10-04T17:00:00.000Z', + stop: '2022-10-04T17:06:00.000Z', + title: 'Adventures With Miao Mi, Ep 1', + episode: 1, + description: + 'When children begin to disappear, a group of young kids have to face their biggest fears when they square off against a murderous, evil clown.' + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + let results = await parser({ + date, + channel, + content, + headers + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/moji.id/moji.id.config.js b/sites/moji.id/moji.id.config.js index d5656ce1..2a942104 100644 --- a/sites/moji.id/moji.id.config.js +++ b/sites/moji.id/moji.id.config.js @@ -1,122 +1,122 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const currentYear = new Date().getFullYear() - -module.exports = { - site: 'moji.id', - days: 4, - output: 'moji.id.guide.xml', - channels: 'moji.id.channels.xml', - lang: 'en', - delay: 5000, - - url: function () { - return 'https://moji.id/schedule' - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 }, - headers: { - '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' - } - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - const items = parseItems(context) - - items.forEach(item => { - programs.push({ - title: item.progTitle, - description: item.progDesc, - start: item.progStart, - stop: item.progStop - }) - }) - - return programs - } -} - -function parseItems(context) { - const $ = cheerio.load(context.content) - const schDayMonths = $('.date-slider .month').toArray() - const schPrograms = $('.desc-slider .list-slider').toArray() - const monthDate = dayjs(context.date).format('MMM DD') - const items = [] - - schDayMonths.forEach(function (schDayMonth, i) { - if (monthDate == $(schDayMonth).text()) { - let schDayPrograms = $(schPrograms[i]).find('.accordion').toArray() - schDayPrograms.forEach(function (program, i) { - let itemDay = { - progStart: parseStart(schDayMonth, program), - progStop: parseStop(schDayMonth, program, schDayPrograms[i + 1]), - progTitle: parseTitle(program), - progDesc: parseDescription(program) - } - items.push(itemDay) - }) - } - }) - - return items -} - -function parseTitle(item) { - return cheerio.load(item)('.name-prog').text() -} - -function parseDescription(item) { - return cheerio.load(item)('.content-acc span').text() -} - -function parseStart(schDayMonth, item) { - let monthDate = cheerio.load(schDayMonth).text().split(' ') - let startTime = cheerio.load(item)('.pkl').text() - let progStart = dayjs.tz( - currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + startTime, - 'YYYY MMM DD HH:mm', - 'Asia/Jakarta' - ) - return progStart -} - -function parseStop(schDayMonth, itemCurrent, itemNext) { - let monthDate = cheerio.load(schDayMonth).text().split(' ') - - if (itemNext) { - let stopTime = cheerio.load(itemNext)('.pkl').text() - return dayjs.tz( - currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + stopTime, - 'YYYY MMM DD HH:mm', - 'Asia/Jakarta' - ) - } else { - return dayjs.tz( - currentYear + - ' ' + - monthDate[0] + - ' ' + - (parseInt(monthDate[1]) + 1).toString().padStart(2, '0') + - ' 00:00', - 'YYYY MMM DD HH:mm', - 'Asia/Jakarta' - ) - } -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const currentYear = new Date().getFullYear() + +module.exports = { + site: 'moji.id', + days: 4, + output: 'moji.id.guide.xml', + channels: 'moji.id.channels.xml', + lang: 'en', + delay: 5000, + + url: function () { + return 'https://moji.id/schedule' + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 }, + headers: { + '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' + } + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + const items = parseItems(context) + + items.forEach(item => { + programs.push({ + title: item.progTitle, + description: item.progDesc, + start: item.progStart, + stop: item.progStop + }) + }) + + return programs + } +} + +function parseItems(context) { + const $ = cheerio.load(context.content) + const schDayMonths = $('.date-slider .month').toArray() + const schPrograms = $('.desc-slider .list-slider').toArray() + const monthDate = dayjs(context.date).format('MMM DD') + const items = [] + + schDayMonths.forEach(function (schDayMonth, i) { + if (monthDate == $(schDayMonth).text()) { + let schDayPrograms = $(schPrograms[i]).find('.accordion').toArray() + schDayPrograms.forEach(function (program, i) { + let itemDay = { + progStart: parseStart(schDayMonth, program), + progStop: parseStop(schDayMonth, program, schDayPrograms[i + 1]), + progTitle: parseTitle(program), + progDesc: parseDescription(program) + } + items.push(itemDay) + }) + } + }) + + return items +} + +function parseTitle(item) { + return cheerio.load(item)('.name-prog').text() +} + +function parseDescription(item) { + return cheerio.load(item)('.content-acc span').text() +} + +function parseStart(schDayMonth, item) { + let monthDate = cheerio.load(schDayMonth).text().split(' ') + let startTime = cheerio.load(item)('.pkl').text() + let progStart = dayjs.tz( + currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + startTime, + 'YYYY MMM DD HH:mm', + 'Asia/Jakarta' + ) + return progStart +} + +function parseStop(schDayMonth, itemCurrent, itemNext) { + let monthDate = cheerio.load(schDayMonth).text().split(' ') + + if (itemNext) { + let stopTime = cheerio.load(itemNext)('.pkl').text() + return dayjs.tz( + currentYear + ' ' + monthDate[0] + ' ' + monthDate[1] + ' ' + stopTime, + 'YYYY MMM DD HH:mm', + 'Asia/Jakarta' + ) + } else { + return dayjs.tz( + currentYear + + ' ' + + monthDate[0] + + ' ' + + (parseInt(monthDate[1]) + 1).toString().padStart(2, '0') + + ' 00:00', + 'YYYY MMM DD HH:mm', + 'Asia/Jakarta' + ) + } +} diff --git a/sites/moji.id/moji.id.test.js b/sites/moji.id/moji.id.test.js index 51182732..93c591ae 100644 --- a/sites/moji.id/moji.id.test.js +++ b/sites/moji.id/moji.id.test.js @@ -1,38 +1,38 @@ -// npm run grab -- --site=moji.id -// npx jest moji.id.test.js - -const { url, parser } = require('./moji.id.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-08-18', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0', - xmltv_id: 'moji.id', - lang: 'en', - logo: 'https://moji.id/site/uploads/logo/62f9387ce00a2-224-x-71.png' -} - -const content = - '

    schedule

    FriAug 18
    SatAug 19
    SunAug 20
    Jam TayangProgram
    00:00TRUST
    Informasi seputar menjaga vitalitas pria
    00:302023 AVC CHALLENGE CUP FOR WOMEN (RECORDED)
    India Vs. Vietnam
    02:30ONE CHAMPIONSHIP 2021
    Siaran laga-laga pertandingan tinju gaya bebas internasional. Meyuguhkan pertarungan sengit dari para petarung profeisional kelas dunia.
    03:30VOLLEYBALL NATION\'S LEAGUE 2023 (RECORDED)
    TURKI vs BRAZIL
    05:00MOJI SPORT
    MOJI SPORT
    06:15LIPUTAN 6 PAGI MOJI
    Kompilasi ragam berita hard news dan soft news baik dari dalam negeri maupun internasional juga info prediksi cuaca di wilayah Indonesia
    07:00UNGKAP
    Liputan investigasi seputar berbagai topik dan peristiwa hangat serta kontroversial yang terjadi di Indonesia
    08:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    10:30SERIES PAGI
    GANTENG GANTENG SERIGALA
    12:30DIAM-DIAM SUKA
    DIAM-DIAM SUKA
    13:30PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    16:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    18:00PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    20:00MOJI DRAMA (CHHOTI SARDARNI)
    CHHOTI SARDARNI
    21:30SINEMA MALAM (BIDADARI CANTIK DI RUMAH KOST)
    (BIDADARI CANTIK DI RUMAH KOST
    23:00TRUST
    Informasi seputar menjaga vitalitas pria
    23:30TRUST
    Informasi seputar menjaga vitalitas pria
    ' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://moji.id/schedule') -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - expect(results).toMatchObject([]) -}) - -it('can parse response', () => { - const results = parser({ content: content, date: date }) - - expect(results[0]).toMatchObject({ - title: 'TRUST', - start: dayjs.tz('2023 Aug 18 00:00', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), - stop: dayjs.tz('2023 Aug 18 00:30', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), - description: 'Informasi seputar menjaga vitalitas pria' - }) -}) +// npm run grab -- --site=moji.id +// npx jest moji.id.test.js + +const { url, parser } = require('./moji.id.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-08-18', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0', + xmltv_id: 'moji.id', + lang: 'en', + logo: 'https://moji.id/site/uploads/logo/62f9387ce00a2-224-x-71.png' +} + +const content = + '

    schedule

    FriAug 18
    SatAug 19
    SunAug 20
    Jam TayangProgram
    00:00TRUST
    Informasi seputar menjaga vitalitas pria
    00:302023 AVC CHALLENGE CUP FOR WOMEN (RECORDED)
    India Vs. Vietnam
    02:30ONE CHAMPIONSHIP 2021
    Siaran laga-laga pertandingan tinju gaya bebas internasional. Meyuguhkan pertarungan sengit dari para petarung profeisional kelas dunia.
    03:30VOLLEYBALL NATION\'S LEAGUE 2023 (RECORDED)
    TURKI vs BRAZIL
    05:00MOJI SPORT
    MOJI SPORT
    06:15LIPUTAN 6 PAGI MOJI
    Kompilasi ragam berita hard news dan soft news baik dari dalam negeri maupun internasional juga info prediksi cuaca di wilayah Indonesia
    07:00UNGKAP
    Liputan investigasi seputar berbagai topik dan peristiwa hangat serta kontroversial yang terjadi di Indonesia
    08:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    10:30SERIES PAGI
    GANTENG GANTENG SERIGALA
    12:30DIAM-DIAM SUKA
    DIAM-DIAM SUKA
    13:30PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    16:00PIALA KAPOLRI 2023 PUTRI (LIVE)
    PIALA KAPOLRI 2023 PUTRI (LIVE)
    18:00PIALA KAPOLRI 2023 PUTRA (LIVE)
    PIALA KAPOLRI 2023 PUTRA (LIVE)
    20:00MOJI DRAMA (CHHOTI SARDARNI)
    CHHOTI SARDARNI
    21:30SINEMA MALAM (BIDADARI CANTIK DI RUMAH KOST)
    (BIDADARI CANTIK DI RUMAH KOST
    23:00TRUST
    Informasi seputar menjaga vitalitas pria
    23:30TRUST
    Informasi seputar menjaga vitalitas pria
    ' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://moji.id/schedule') +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + expect(results).toMatchObject([]) +}) + +it('can parse response', () => { + const results = parser({ content: content, date: date }) + + expect(results[0]).toMatchObject({ + title: 'TRUST', + start: dayjs.tz('2023 Aug 18 00:00', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), + stop: dayjs.tz('2023 Aug 18 00:30', 'YYYY MMM DD HH:mm', 'Asia/Jakarta'), + description: 'Informasi seputar menjaga vitalitas pria' + }) +}) diff --git a/sites/mon-programme-tv.be/mon-programme-tv.be.config.js b/sites/mon-programme-tv.be/mon-programme-tv.be.config.js index 72c8f6b3..1c359144 100644 --- a/sites/mon-programme-tv.be/mon-programme-tv.be.config.js +++ b/sites/mon-programme-tv.be/mon-programme-tv.be.config.js @@ -1,100 +1,100 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'mon-programme-tv.be', - days: 2, - url({ date, channel }) { - return `https://www.mon-programme-tv.be/chaine/${date.format('DDMMYYYY')}/${ - channel.site_id - }.html` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.mon-programme-tv.be/chaine/toutes-les-chaines-television.html') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - - const channels = [] - $('.list-chaines > ul > li').each((i, el) => { - const [, site_id] = $(el) - .find('a') - .attr('href') - .match(/\/chaine\/(.*).html/) || [null, null] - const [, name] = $(el) - .find('a') - .attr('title') - .match(/Programme TV ce soir (.*)/) || [null, null] - - channels.push({ - site_id, - name, - lang: 'fr' - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseDescription($item) { - return $item('.episode').text().trim() -} - -function parseCategory($item) { - return $item('.type').text().trim() -} - -function parseIcon($item) { - return $item('.image img').data('src') -} - -function parseStart($item, date) { - const time = $item('.hour').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Brussels') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.box').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'mon-programme-tv.be', + days: 2, + url({ date, channel }) { + return `https://www.mon-programme-tv.be/chaine/${date.format('DDMMYYYY')}/${ + channel.site_id + }.html` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.mon-programme-tv.be/chaine/toutes-les-chaines-television.html') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + + const channels = [] + $('.list-chaines > ul > li').each((i, el) => { + const [, site_id] = $(el) + .find('a') + .attr('href') + .match(/\/chaine\/(.*).html/) || [null, null] + const [, name] = $(el) + .find('a') + .attr('title') + .match(/Programme TV ce soir (.*)/) || [null, null] + + channels.push({ + site_id, + name, + lang: 'fr' + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseDescription($item) { + return $item('.episode').text().trim() +} + +function parseCategory($item) { + return $item('.type').text().trim() +} + +function parseIcon($item) { + return $item('.image img').data('src') +} + +function parseStart($item, date) { + const time = $item('.hour').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Brussels') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.box').toArray() +} diff --git a/sites/mon-programme-tv.be/mon-programme-tv.be.test.js b/sites/mon-programme-tv.be/mon-programme-tv.be.test.js index c9f56deb..9f78271d 100644 --- a/sites/mon-programme-tv.be/mon-programme-tv.be.test.js +++ b/sites/mon-programme-tv.be/mon-programme-tv.be.test.js @@ -1,66 +1,66 @@ -// npm run channels:parse -- --config=./sites/mon-programme-tv.be/mon-programme-tv.be.config.js --output=./sites/mon-programme-tv.be/mon-programme-tv.be.channels.xml -// npm run grab -- --site=mon-programme-tv.be - -const { parser, url } = require('./mon-programme-tv.be.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1873/programme-television-ln24', - xmltv_id: 'LN24.be' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.mon-programme-tv.be/chaine/19012023/1873/programme-television-ln24.html' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-19T05:30:00.000Z', - stop: '2023-01-19T05:55:00.000Z', - title: 'LN Matin', - category: 'Magazine Actualité', - icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Reportage_1.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2023-01-19T05:55:00.000Z', - stop: '2023-01-19T06:00:00.000Z', - title: 'Météo', - category: 'Météo', - icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Meteo.jpg' - }) - - expect(results[8]).toMatchObject({ - start: '2023-01-19T08:00:00.000Z', - stop: '2023-01-19T08:05:00.000Z', - title: 'Le journal', - description: "L'information de la mi-journée avec des JT...", - category: 'Journal', - icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/journal.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - date - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/mon-programme-tv.be/mon-programme-tv.be.config.js --output=./sites/mon-programme-tv.be/mon-programme-tv.be.channels.xml +// npm run grab -- --site=mon-programme-tv.be + +const { parser, url } = require('./mon-programme-tv.be.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1873/programme-television-ln24', + xmltv_id: 'LN24.be' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.mon-programme-tv.be/chaine/19012023/1873/programme-television-ln24.html' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-19T05:30:00.000Z', + stop: '2023-01-19T05:55:00.000Z', + title: 'LN Matin', + category: 'Magazine Actualité', + icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Reportage_1.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2023-01-19T05:55:00.000Z', + stop: '2023-01-19T06:00:00.000Z', + title: 'Météo', + category: 'Météo', + icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/Meteo.jpg' + }) + + expect(results[8]).toMatchObject({ + start: '2023-01-19T08:00:00.000Z', + stop: '2023-01-19T08:05:00.000Z', + title: 'Le journal', + description: "L'information de la mi-journée avec des JT...", + category: 'Journal', + icon: 'https://dnsmptv-img.pragma-consult.be/imgs/picto/132/journal.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/movistarplus.es/movistarplus.es.config.js b/sites/movistarplus.es/movistarplus.es.config.js index d0c1f9ac..a6493c71 100644 --- a/sites/movistarplus.es/movistarplus.es.config.js +++ b/sites/movistarplus.es/movistarplus.es.config.js @@ -1,49 +1,49 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'movistarplus.es', - days: 2, - url: function ({ date }) { - return `https://www.movistarplus.es/programacion-tv/${date.format('YYYY-MM-DD')}?v=json` - }, - parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - let guideDate = date - items.forEach(item => { - let startTime = DateTime.fromFormat( - `${guideDate.format('YYYY-MM-DD')} ${item.HORA_INICIO}`, - 'yyyy-MM-dd HH:mm', - { - zone: 'Europe/Madrid' - } - ).toUTC() - let stopTime = DateTime.fromFormat( - `${guideDate.format('YYYY-MM-DD')} ${item.HORA_FIN}`, - 'yyyy-MM-dd HH:mm', - { - zone: 'Europe/Madrid' - } - ).toUTC() - if (stopTime < startTime) { - guideDate = guideDate.add(1, 'd') - stopTime = stopTime.plus({ days: 1 }) - } - programs.push({ - title: item.TITULO, - category: item.GENERO, - start: startTime, - stop: stopTime - }) - }) - return programs - } -} - -function parseItems(content, channel) { - const json = typeof content === 'string' ? JSON.parse(content) : content - if (!(`${channel.site_id}-CODE` in json.data)) return [] - const data = json.data[`${channel.site_id}-CODE`] - return data ? data.PROGRAMAS : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'movistarplus.es', + days: 2, + url: function ({ date }) { + return `https://www.movistarplus.es/programacion-tv/${date.format('YYYY-MM-DD')}?v=json` + }, + parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + let guideDate = date + items.forEach(item => { + let startTime = DateTime.fromFormat( + `${guideDate.format('YYYY-MM-DD')} ${item.HORA_INICIO}`, + 'yyyy-MM-dd HH:mm', + { + zone: 'Europe/Madrid' + } + ).toUTC() + let stopTime = DateTime.fromFormat( + `${guideDate.format('YYYY-MM-DD')} ${item.HORA_FIN}`, + 'yyyy-MM-dd HH:mm', + { + zone: 'Europe/Madrid' + } + ).toUTC() + if (stopTime < startTime) { + guideDate = guideDate.add(1, 'd') + stopTime = stopTime.plus({ days: 1 }) + } + programs.push({ + title: item.TITULO, + category: item.GENERO, + start: startTime, + stop: stopTime + }) + }) + return programs + } +} + +function parseItems(content, channel) { + const json = typeof content === 'string' ? JSON.parse(content) : content + if (!(`${channel.site_id}-CODE` in json.data)) return [] + const data = json.data[`${channel.site_id}-CODE`] + return data ? data.PROGRAMAS : [] +} diff --git a/sites/movistarplus.es/movistarplus.es.test.js b/sites/movistarplus.es/movistarplus.es.test.js index 88f665b2..de824578 100644 --- a/sites/movistarplus.es/movistarplus.es.test.js +++ b/sites/movistarplus.es/movistarplus.es.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=movistarplus.es - -const { parser, url } = require('./movistarplus.es.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'TVE', - xmltv_id: 'SomeChannel.es' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.movistarplus.es/programacion-tv/2022-03-11?v=json') -}) - -it('can parse response', () => { - const content = - '{"success":"true","msg":"","data":{"TVE-CODE":{"DATOS_CADENA":{"CODIGO":"TVE","MARCA":"TVE","NOMBRE":"LA 1","URL":"https://www.movistarplus.es/canal?nombre=LA%2B1&id=TVE","DIAL_PRINCIPAL":["01"],"DIALES":[1],"UID":null,"CASID":null,"SERVICEUID":null,"SERVICEUID2":null,"SERVICEID":null,"ESVIRTUAL":null,"ESSATELITE":null,"UPSELLING":null,"puntoReproduccion":null},"PROGRAMAS":[{"DIRECTO":false,"TEMPORADA":"","TITULO":"Telediario Matinal","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":150,"DURACION_VISUAL":150,"HORA_INICIO":"06:00","HORA_FIN":"08:30","ELEMENTO":"1709045","EVENTO":"99422566","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/telediario-matinal?tipo=R&id=99422566"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Las Claves del Siglo XXI: Episodio 8","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":135,"DURACION_VISUAL":135,"HORA_INICIO":"22:15","HORA_FIN":"00:30","ELEMENTO":"2051356","EVENTO":"99422634","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/las-claves-del-siglo-xxi-t1/episodio-8?tipo=R&id=99422634"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Noticias 24H","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":170,"DURACION_VISUAL":170,"HORA_INICIO":"03:10","HORA_FIN":"06:00","ELEMENTO":"518403","EVENTO":"99422646","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/noticias-24h?tipo=R&id=99422646"}]}}}' - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2022-03-11T05:00:00.000Z', - stop: '2022-03-11T07:30:00.000Z', - category: 'Información', - title: 'Telediario Matinal' - }, - { - start: '2022-03-11T21:15:00.000Z', - stop: '2022-03-11T23:30:00.000Z', - category: 'Información', - title: 'Las Claves del Siglo XXI: Episodio 8' - }, - { - start: '2022-03-12T02:10:00.000Z', - stop: '2022-03-12T05:00:00.000Z', - category: 'Información', - title: 'Noticias 24H' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"success":"true","msg":"","data":{}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=movistarplus.es + +const { parser, url } = require('./movistarplus.es.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'TVE', + xmltv_id: 'SomeChannel.es' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.movistarplus.es/programacion-tv/2022-03-11?v=json') +}) + +it('can parse response', () => { + const content = + '{"success":"true","msg":"","data":{"TVE-CODE":{"DATOS_CADENA":{"CODIGO":"TVE","MARCA":"TVE","NOMBRE":"LA 1","URL":"https://www.movistarplus.es/canal?nombre=LA%2B1&id=TVE","DIAL_PRINCIPAL":["01"],"DIALES":[1],"UID":null,"CASID":null,"SERVICEUID":null,"SERVICEUID2":null,"SERVICEID":null,"ESVIRTUAL":null,"ESSATELITE":null,"UPSELLING":null,"puntoReproduccion":null},"PROGRAMAS":[{"DIRECTO":false,"TEMPORADA":"","TITULO":"Telediario Matinal","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":150,"DURACION_VISUAL":150,"HORA_INICIO":"06:00","HORA_FIN":"08:30","ELEMENTO":"1709045","EVENTO":"99422566","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/telediario-matinal?tipo=R&id=99422566"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Las Claves del Siglo XXI: Episodio 8","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":135,"DURACION_VISUAL":135,"HORA_INICIO":"22:15","HORA_FIN":"00:30","ELEMENTO":"2051356","EVENTO":"99422634","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/las-claves-del-siglo-xxi-t1/episodio-8?tipo=R&id=99422634"},{"DIRECTO":false,"TEMPORADA":"","TITULO":"Noticias 24H","GENERO":"Información","CODIGO_GENERO":"IF","DURACION":170,"DURACION_VISUAL":170,"HORA_INICIO":"03:10","HORA_FIN":"06:00","ELEMENTO":"518403","EVENTO":"99422646","ShowId":null,"x1":0,"x2":0,"Disponible":null,"URL":"https://www.movistarplus.es/ficha/noticias-24h?tipo=R&id=99422646"}]}}}' + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2022-03-11T05:00:00.000Z', + stop: '2022-03-11T07:30:00.000Z', + category: 'Información', + title: 'Telediario Matinal' + }, + { + start: '2022-03-11T21:15:00.000Z', + stop: '2022-03-11T23:30:00.000Z', + category: 'Información', + title: 'Las Claves del Siglo XXI: Episodio 8' + }, + { + start: '2022-03-12T02:10:00.000Z', + stop: '2022-03-12T05:00:00.000Z', + category: 'Información', + title: 'Noticias 24H' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"success":"true","msg":"","data":{}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mtel.ba/mtel.ba.config.js b/sites/mtel.ba/mtel.ba.config.js index 33f13ab4..aa355a5f 100644 --- a/sites/mtel.ba/mtel.ba.config.js +++ b/sites/mtel.ba/mtel.ba.config.js @@ -1,58 +1,58 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) - -module.exports = { - site: 'mtel.ba', - days: 2, - url: function ({ channel, date }) { - const [position] = channel.site_id.split('#') - - return `https://mtel.ba/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` - }, - request: { - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - if (item.title === 'Nema informacija o programu') return - programs.push({ - title: item.title, - description: item.description, - category: item.category, - icon: item.image, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.tz(item.full_start, 'Europe/Sarajevo') -} - -function parseStop(item) { - return dayjs.tz(item.full_end, 'Europe/Sarajevo') -} - -function parseContent(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return null - - return data.channels.find(i => i.id === channelId) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - - return data ? data.items : [] -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) + +module.exports = { + site: 'mtel.ba', + days: 2, + url: function ({ channel, date }) { + const [position] = channel.site_id.split('#') + + return `https://mtel.ba/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` + }, + request: { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + if (item.title === 'Nema informacija o programu') return + programs.push({ + title: item.title, + description: item.description, + category: item.category, + icon: item.image, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.tz(item.full_start, 'Europe/Sarajevo') +} + +function parseStop(item) { + return dayjs.tz(item.full_end, 'Europe/Sarajevo') +} + +function parseContent(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return null + + return data.channels.find(i => i.id === channelId) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + + return data ? data.items : [] +} diff --git a/sites/mtel.ba/mtel.ba.test.js b/sites/mtel.ba/mtel.ba.test.js index 765979ee..6b4598df 100644 --- a/sites/mtel.ba/mtel.ba.test.js +++ b/sites/mtel.ba/mtel.ba.test.js @@ -1,53 +1,53 @@ -// npm run grab -- --site=mtel.ba - -const { parser, url, request } = require('./mtel.ba.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '001#11', - xmltv_id: 'RTRSTV.ba' -} -const content = - '{"page":0,"total_pages":1,"date":"2021-11-10","channels":[{"id":"11","name":"RTRS","description":null,"link":null,"image":"https://mtel.ba/oec/images/tv_channels/c3556aa629b00325aaaea622abfb1070.png","position":"001","items":[{"id_channel":"11","title":"\u0160uma","description":"Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi u\u010dvrstili novo socijalisti\u010dko prijateljstvo, rumunski zvani\u010dnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom \u0160uma. Mnogo godina kasnije ta slika je umje\u0161ana u napetu \u0161pijunsku pri\u010du i otkriva tajnu koja \u0107e uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji \u010dine ono \u0161to zovemo stvarno\u0161\u0107u.","start":"00:00:00","duration":"46.00","full_start":"2021-11-09 23:29:00","full_end":"2021-11-10 00:46:00","image":"https://mtel.ba/oec/images/epg/60881491.jpg","category":"Televizijski film","subcategory":"Dokumentarna drama"},{"id_channel":"11","title":"Nema informacija o programu","description":"","start":"07:32:00","duration":"988.00","full_start":"2021-11-10 07:32:00","full_end":"2021-11-10 24:00:00","image":"","category":"","subcategory":""}]}]}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://mtel.ba/oec/epg/program?date=2021-11-10&position=001' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'X-Requested-With': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-09T22:29:00.000Z', - stop: '2021-11-09T23:46:00.000Z', - title: 'Šuma', - icon: 'https://mtel.ba/oec/images/epg/60881491.jpg', - description: - 'Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi učvrstili novo socijalističko prijateljstvo, rumunski zvaničnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom Šuma. Mnogo godina kasnije ta slika je umješana u napetu špijunsku priču i otkriva tajnu koja će uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji čine ono što zovemo stvarnošću.', - category: 'Televizijski film' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"message":"Tra\u017eeni termin nije prona\u0111en.\u003Cbr\u003E\u003Cbr\u003EProverite da li ste upisali pravilno ili poku\u0161ajte sa nekim drugim terminom."}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mtel.ba + +const { parser, url, request } = require('./mtel.ba.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '001#11', + xmltv_id: 'RTRSTV.ba' +} +const content = + '{"page":0,"total_pages":1,"date":"2021-11-10","channels":[{"id":"11","name":"RTRS","description":null,"link":null,"image":"https://mtel.ba/oec/images/tv_channels/c3556aa629b00325aaaea622abfb1070.png","position":"001","items":[{"id_channel":"11","title":"\u0160uma","description":"Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi u\u010dvrstili novo socijalisti\u010dko prijateljstvo, rumunski zvani\u010dnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom \u0160uma. Mnogo godina kasnije ta slika je umje\u0161ana u napetu \u0161pijunsku pri\u010du i otkriva tajnu koja \u0107e uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji \u010dine ono \u0161to zovemo stvarno\u0161\u0107u.","start":"00:00:00","duration":"46.00","full_start":"2021-11-09 23:29:00","full_end":"2021-11-10 00:46:00","image":"https://mtel.ba/oec/images/epg/60881491.jpg","category":"Televizijski film","subcategory":"Dokumentarna drama"},{"id_channel":"11","title":"Nema informacija o programu","description":"","start":"07:32:00","duration":"988.00","full_start":"2021-11-10 07:32:00","full_end":"2021-11-10 24:00:00","image":"","category":"","subcategory":""}]}]}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://mtel.ba/oec/epg/program?date=2021-11-10&position=001' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'X-Requested-With': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-09T22:29:00.000Z', + stop: '2021-11-09T23:46:00.000Z', + title: 'Šuma', + icon: 'https://mtel.ba/oec/images/epg/60881491.jpg', + description: + 'Krajem decembra 1947. godine jugoslovenski predsjednik Josip Broz Tito prvi put je posjetio Rumuniju. Da bi učvrstili novo socijalističko prijateljstvo, rumunski zvaničnici su poklonili Titu sliku velikog rumunskog umjetnika Jona Andreskua pod nazivom Šuma. Mnogo godina kasnije ta slika je umješana u napetu špijunsku priču i otkriva tajnu koja će uzdrmati temelje i Jugoslavije i Rumunije. Film je svjedok kompleksnosti i raznovrsnosti glasova koji čine ono što zovemo stvarnošću.', + category: 'Televizijski film' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"message":"Tra\u017eeni termin nije prona\u0111en.\u003Cbr\u003E\u003Cbr\u003EProverite da li ste upisali pravilno ili poku\u0161ajte sa nekim drugim terminom."}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mts.rs/mts.rs.config.js b/sites/mts.rs/mts.rs.config.js index 00d3b552..b1382980 100644 --- a/sites/mts.rs/mts.rs.config.js +++ b/sites/mts.rs/mts.rs.config.js @@ -1,63 +1,63 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'mts.rs', - days: 2, - url({ date, channel }) { - const [position] = channel.site_id.split('#') - - return `https://mts.rs/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` - }, - request: { - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }, - parser: function ({ content, channel }) { - let programs = [] - const data = parseContent(content, channel) - const items = parseItems(data) - items.forEach(item => { - programs.push({ - title: item.title, - category: item.category, - description: item.description, - icon: item.image, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseContent(content, channel) { - const [, site_id] = channel.site_id.split('#') - let data - try { - data = JSON.parse(content) - } catch (error) { - console.log(error) - } - if (!data || !data.channels || !data.channels.length) return null - - return data.channels.find(c => c.id === site_id) || null -} - -function parseStart(item) { - return dayjs.tz(item.full_start, 'Europe/Belgrade') -} - -function parseStop(item) { - return dayjs.tz(item.full_end, 'Europe/Belgrade') -} - -function parseItems(data) { - return data && Array.isArray(data.items) ? data.items : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'mts.rs', + days: 2, + url({ date, channel }) { + const [position] = channel.site_id.split('#') + + return `https://mts.rs/oec/epg/program?date=${date.format('YYYY-MM-DD')}&position=${position}` + }, + request: { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }, + parser: function ({ content, channel }) { + let programs = [] + const data = parseContent(content, channel) + const items = parseItems(data) + items.forEach(item => { + programs.push({ + title: item.title, + category: item.category, + description: item.description, + icon: item.image, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseContent(content, channel) { + const [, site_id] = channel.site_id.split('#') + let data + try { + data = JSON.parse(content) + } catch (error) { + console.log(error) + } + if (!data || !data.channels || !data.channels.length) return null + + return data.channels.find(c => c.id === site_id) || null +} + +function parseStart(item) { + return dayjs.tz(item.full_start, 'Europe/Belgrade') +} + +function parseStop(item) { + return dayjs.tz(item.full_end, 'Europe/Belgrade') +} + +function parseItems(data) { + return data && Array.isArray(data.items) ? data.items : [] +} diff --git a/sites/mts.rs/mts.rs.test.js b/sites/mts.rs/mts.rs.test.js index 87ff70cc..836487c8 100644 --- a/sites/mts.rs/mts.rs.test.js +++ b/sites/mts.rs/mts.rs.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=mts.rs - -const { parser, url, request } = require('./mts.rs.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '101#597', - xmltv_id: 'RTS1.rs' -} -const content = - '{"page":0,"total_pages":1,"date":"2021-11-07","channels":[{"id":"597","name":"RTS 1","description":null,"link":null,"image":"https://mts.rs/oec/images/tv_channels/904ddd8cd6720a4a1c23eae513b5b957.jpg","position":"101","positions":"101","items":[{"id_channel":"597","title":"Zaboravljeni zlo\u010din","description":"Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.","start":"00:00:00","duration":"103.00","full_start":"2021-11-06 23:44:00","full_end":"2021-11-07 01:43:00","image":"https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg","category":"Bioskopski film","subcategory":""}]}]}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://mts.rs/oec/epg/program?date=2021-11-07&position=101') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'X-Requested-With': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-06T22:44:00.000Z', - stop: '2021-11-07T00:43:00.000Z', - title: 'Zaboravljeni zlo\u010din', - category: 'Bioskopski film', - icon: 'https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg', - description: - 'Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"message":"Nema rezultata."}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mts.rs + +const { parser, url, request } = require('./mts.rs.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '101#597', + xmltv_id: 'RTS1.rs' +} +const content = + '{"page":0,"total_pages":1,"date":"2021-11-07","channels":[{"id":"597","name":"RTS 1","description":null,"link":null,"image":"https://mts.rs/oec/images/tv_channels/904ddd8cd6720a4a1c23eae513b5b957.jpg","position":"101","positions":"101","items":[{"id_channel":"597","title":"Zaboravljeni zlo\u010din","description":"Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.","start":"00:00:00","duration":"103.00","full_start":"2021-11-06 23:44:00","full_end":"2021-11-07 01:43:00","image":"https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg","category":"Bioskopski film","subcategory":""}]}]}' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://mts.rs/oec/epg/program?date=2021-11-07&position=101') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'X-Requested-With': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-06T22:44:00.000Z', + stop: '2021-11-07T00:43:00.000Z', + title: 'Zaboravljeni zlo\u010din', + category: 'Bioskopski film', + icon: 'https://mts.rs/oec/images/epg/2_abb81cc24d8ce957eece50f991a31e59780e4e53_E7D8ECDE568E84E3C86CCDBDB647355E.jpg', + description: + 'Novinarka-fotoreporter, D\u017ein, istra\u017euje okrutno i senzacionalno, nere\u0161eno ubistvo sekirom iz davne 1873. godine. Ubistvo koje koincidira sa nedavnim identi\u010dnim brutalnim dvostrukim ubistvom. Zaplet se odvija izme\u0111u pri\u010de o\u010devica iz toga doba - pri\u010de iz novinske arhive i D\u017einine privatne borbe sa ljubomorom i sumnjom koje prate njen brak.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"message":"Nema rezultata."}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mujtvprogram.cz/mujtvprogram.cz.config.js b/sites/mujtvprogram.cz/mujtvprogram.cz.config.js index 8e9f0caa..1b83c1f6 100644 --- a/sites/mujtvprogram.cz/mujtvprogram.cz.config.js +++ b/sites/mujtvprogram.cz/mujtvprogram.cz.config.js @@ -1,69 +1,69 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const convert = require('xml-js') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mujtvprogram.cz', - days: 2, - url({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') - return `https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=${channel.site_id}&day=${diff}` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name._text, - start: parseTime(item.startDate._text), - stop: parseTime(item.endDate._text), - description: parseDescription(item), - category: parseCategory(item), - date: item.year._text || null, - director: parseList(item.directors), - actor: parseList(item.actors) - }) - }) - return programs - } -} - -function parseItems(content) { - try { - const data = convert.xml2js(content, { - compact: true, - ignoreDeclaration: true, - ignoreAttributes: true - }) - if (!data) return [] - const programmes = data['tv-program-programmes'].programme - return programmes && Array.isArray(programmes) ? programmes : [] - } catch (err) { - return [] - } -} -function parseDescription(item) { - if (item.longDescription) return item.longDescription._text - if (item.shortDescription) return item.shortDescription._text - return null -} - -function parseList(list) { - if (!list) return [] - if (!list._text) return [] - return typeof list._text === 'string' ? list._text.split(', ') : [] -} -function parseTime(time) { - return dayjs.tz(time, 'DD.MM.YYYY HH.mm', 'Europe/Prague') -} - -function parseCategory(item) { - if (!item['programme-type']) return null - return item['programme-type'].name._text -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const convert = require('xml-js') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mujtvprogram.cz', + days: 2, + url({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + return `https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=${channel.site_id}&day=${diff}` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name._text, + start: parseTime(item.startDate._text), + stop: parseTime(item.endDate._text), + description: parseDescription(item), + category: parseCategory(item), + date: item.year._text || null, + director: parseList(item.directors), + actor: parseList(item.actors) + }) + }) + return programs + } +} + +function parseItems(content) { + try { + const data = convert.xml2js(content, { + compact: true, + ignoreDeclaration: true, + ignoreAttributes: true + }) + if (!data) return [] + const programmes = data['tv-program-programmes'].programme + return programmes && Array.isArray(programmes) ? programmes : [] + } catch (err) { + return [] + } +} +function parseDescription(item) { + if (item.longDescription) return item.longDescription._text + if (item.shortDescription) return item.shortDescription._text + return null +} + +function parseList(list) { + if (!list) return [] + if (!list._text) return [] + return typeof list._text === 'string' ? list._text.split(', ') : [] +} +function parseTime(time) { + return dayjs.tz(time, 'DD.MM.YYYY HH.mm', 'Europe/Prague') +} + +function parseCategory(item) { + if (!item['programme-type']) return null + return item['programme-type'].name._text +} diff --git a/sites/mujtvprogram.cz/mujtvprogram.cz.test.js b/sites/mujtvprogram.cz/mujtvprogram.cz.test.js index c15d2329..6cf65637 100644 --- a/sites/mujtvprogram.cz/mujtvprogram.cz.test.js +++ b/sites/mujtvprogram.cz/mujtvprogram.cz.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=mujtvprogram.cz - -const { parser, url } = require('./mujtvprogram.cz.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - site_id: '1', - xmltv_id: 'CT1.cz' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=0' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=1' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results[3]).toMatchObject({ - title: 'Čepice', - description: - 'Jarka (J. Bohdalová) vyčítá manželovi Jiřímu (F. Řehák), že jí nepomáhá při předvánočním úklidu. Vzápětí ale náhodou najde ve skříni ukrytou dámskou čepici a napadne ji, že jde o Jiřího dárek pro ni pod stromeček. Její chování se ihned změní. Jen muži naznačí, že by chtěla čepici jiné barvy. Manžel jí ovšem řekne, že čepici si u něj schoval kamarád Venca (M. Šulc). Zklamaná žena to prozradí Vencově manželce Božce (A. Tománková). Na Štědrý den však Božka najde pod stromečkem jen rtěnku...', - category: 'film', - date: '1983', - director: ['Mudra F.'], - actor: ['Bohdalová J.', 'Řehák F.', 'Šulc M.'], - start: '2022-12-23T08:00:00.000Z', - stop: '2022-12-23T08:20:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser(content, channel) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mujtvprogram.cz + +const { parser, url } = require('./mujtvprogram.cz.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const channel = { + site_id: '1', + xmltv_id: 'CT1.cz' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=0' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://services.mujtvprogram.cz/tvprogram2services/services/tvprogrammelist_mobile.php?channel_cid=1&day=1' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results[3]).toMatchObject({ + title: 'Čepice', + description: + 'Jarka (J. Bohdalová) vyčítá manželovi Jiřímu (F. Řehák), že jí nepomáhá při předvánočním úklidu. Vzápětí ale náhodou najde ve skříni ukrytou dámskou čepici a napadne ji, že jde o Jiřího dárek pro ni pod stromeček. Její chování se ihned změní. Jen muži naznačí, že by chtěla čepici jiné barvy. Manžel jí ovšem řekne, že čepici si u něj schoval kamarád Venca (M. Šulc). Zklamaná žena to prozradí Vencově manželce Božce (A. Tománková). Na Štědrý den však Božka najde pod stromečkem jen rtěnku...', + category: 'film', + date: '1983', + director: ['Mudra F.'], + actor: ['Bohdalová J.', 'Řehák F.', 'Šulc M.'], + start: '2022-12-23T08:00:00.000Z', + stop: '2022-12-23T08:20:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser(content, channel) + expect(result).toMatchObject([]) +}) diff --git a/sites/musor.tv/musor.tv.config.js b/sites/musor.tv/musor.tv.config.js index 85568d9f..6a58e6b4 100644 --- a/sites/musor.tv/musor.tv.config.js +++ b/sites/musor.tv/musor.tv.config.js @@ -1,87 +1,87 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const axios = require('axios') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'musor.tv', - days: 2, - url({ channel, date }) { - return dayjs.utc().isSame(date, 'd') - ? `https://musor.tv/mai/tvmusor/${channel.site_id}` - : `https://musor.tv/napi/tvmusor/${channel.site_id}/${date.format('YYYY.MM.DD')}` - }, - parser({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item) - if (prev) prev.stop = start - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://musor.tv/') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const channels = $('body > div.big_content > div > nav > table > tbody > tr > td > a').toArray() - return channels - .map(item => { - const $item = cheerio.load(item) - const url = $item('*').attr('href') - if (!url.startsWith('//musor.tv/mai/tvmusor/')) return null - const site_id = url.replace('//musor.tv/mai/tvmusor/', '') - return { - lang: 'hu', - site_id, - name: $item('*').text() - } - }) - .filter(i => i) - } -} - -function parseIcon($item) { - const imgSrc = $item('div.smartpe_screenshot > img').attr('src') - - return imgSrc ? `https:${imgSrc}` : null -} - -function parseTitle($item) { - return $item('div:nth-child(2) > div > h3 > a').text().trim() -} - -function parseDescription($item) { - return $item('div:nth-child(5) > div > div').text().trim() -} - -function parseStart($item) { - let datetime = $item('div:nth-child(1) > div > div > div > div > time').attr('content') - if (!datetime) return null - - return dayjs.utc(datetime.replace('GMT', 'T'), 'YYYY-MM-DDTHH:mm:ss') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('div.multicolumndayprogarea > div.smartpe_progentry').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const axios = require('axios') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'musor.tv', + days: 2, + url({ channel, date }) { + return dayjs.utc().isSame(date, 'd') + ? `https://musor.tv/mai/tvmusor/${channel.site_id}` + : `https://musor.tv/napi/tvmusor/${channel.site_id}/${date.format('YYYY.MM.DD')}` + }, + parser({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item) + if (prev) prev.stop = start + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://musor.tv/') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const channels = $('body > div.big_content > div > nav > table > tbody > tr > td > a').toArray() + return channels + .map(item => { + const $item = cheerio.load(item) + const url = $item('*').attr('href') + if (!url.startsWith('//musor.tv/mai/tvmusor/')) return null + const site_id = url.replace('//musor.tv/mai/tvmusor/', '') + return { + lang: 'hu', + site_id, + name: $item('*').text() + } + }) + .filter(i => i) + } +} + +function parseIcon($item) { + const imgSrc = $item('div.smartpe_screenshot > img').attr('src') + + return imgSrc ? `https:${imgSrc}` : null +} + +function parseTitle($item) { + return $item('div:nth-child(2) > div > h3 > a').text().trim() +} + +function parseDescription($item) { + return $item('div:nth-child(5) > div > div').text().trim() +} + +function parseStart($item) { + let datetime = $item('div:nth-child(1) > div > div > div > div > time').attr('content') + if (!datetime) return null + + return dayjs.utc(datetime.replace('GMT', 'T'), 'YYYY-MM-DDTHH:mm:ss') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('div.multicolumndayprogarea > div.smartpe_progentry').toArray() +} diff --git a/sites/musor.tv/musor.tv.test.js b/sites/musor.tv/musor.tv.test.js index 6caa143a..4c07d8c2 100644 --- a/sites/musor.tv/musor.tv.test.js +++ b/sites/musor.tv/musor.tv.test.js @@ -1,60 +1,60 @@ -// npm run channels:parse -- --config=./sites/musor.tv/musor.tv.config.js --output=./sites/musor.tv/musor.tv.channels.xml -// npm run grab -- --site=musor.tv - -const { parser, url } = require('./musor.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'HATOS_CSATORNA', - xmltv_id: 'Hatoscsatorna.hu' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://musor.tv/napi/tvmusor/HATOS_CSATORNA/2022.11.19') -}) - -it('can generate valid url for today', () => { - const today = dayjs.utc().startOf('d') - - expect(url({ channel, date: today })).toBe('https://musor.tv/mai/tvmusor/HATOS_CSATORNA') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-19T23:00:00.000Z', - stop: '2022-11-19T23:30:00.000Z', - title: 'Egészségtér', - description: - 'Egészségtér címmel új természetgyógyászattal foglalkozó magazinműsor indult hetente fél órás időtartamban a hatoscsatornán. A műsor derűs, objektív hangvételével és szakmailag magas színvonalú ismeretterjesztő jellegével az e' - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-19T23:30:00.000Z', - stop: '2022-11-20T00:00:00.000Z', - title: 'Tradíció Klipek', - description: 'Tradíció Klipek Birinyi József néprajzi, vallási, népzenei, népszokás filmjeiből.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/musor.tv/musor.tv.config.js --output=./sites/musor.tv/musor.tv.channels.xml +// npm run grab -- --site=musor.tv + +const { parser, url } = require('./musor.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'HATOS_CSATORNA', + xmltv_id: 'Hatoscsatorna.hu' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://musor.tv/napi/tvmusor/HATOS_CSATORNA/2022.11.19') +}) + +it('can generate valid url for today', () => { + const today = dayjs.utc().startOf('d') + + expect(url({ channel, date: today })).toBe('https://musor.tv/mai/tvmusor/HATOS_CSATORNA') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-19T23:00:00.000Z', + stop: '2022-11-19T23:30:00.000Z', + title: 'Egészségtér', + description: + 'Egészségtér címmel új természetgyógyászattal foglalkozó magazinműsor indult hetente fél órás időtartamban a hatoscsatornán. A műsor derűs, objektív hangvételével és szakmailag magas színvonalú ismeretterjesztő jellegével az e' + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-19T23:30:00.000Z', + stop: '2022-11-20T00:00:00.000Z', + title: 'Tradíció Klipek', + description: 'Tradíció Klipek Birinyi József néprajzi, vallási, népzenei, népszokás filmjeiből.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js index 0969598d..d2de4c3f 100644 --- a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js +++ b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js @@ -1,76 +1,76 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -module.exports = { - site: 'myafn.dodmedia.osd.mil', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `https://v3.myafn.dodmedia.osd.mil/api/json/32/${date.format( - 'YYYY-MM-DD' - )}@0000/${date.format('YYYY-MM-DD')}@2359/schedule.json` - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.o, 'm') - programs.push({ - title: item.h, - sub_title: item.i, - description: item.l, - rating: parseRating(item), - category: parseCategory(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://v3.myafn.dodmedia.osd.mil/api/json/32/channels.json') - .then(r => r.data) - .catch(console.log) - - return data.map(item => ({ - site_id: item.Channel, - name: item.Title - })) - } -} - -function parseStart(item) { - return dayjs.utc(item.e, 'YYYY,M,D,H,m,s,0').add(1, 'month') -} - -function parseCategory(item) { - return item.m ? item.m.split(',') : [] -} - -function parseRating(item) { - return item.j - ? { - system: 'MPA', - value: item.j - } - : null -} - -function parseItems(content, channel) { - const items = JSON.parse(content) - if (!Array.isArray(items)) return [] - - return items.filter(i => i.b == channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +module.exports = { + site: 'myafn.dodmedia.osd.mil', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `https://v3.myafn.dodmedia.osd.mil/api/json/32/${date.format( + 'YYYY-MM-DD' + )}@0000/${date.format('YYYY-MM-DD')}@2359/schedule.json` + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.o, 'm') + programs.push({ + title: item.h, + sub_title: item.i, + description: item.l, + rating: parseRating(item), + category: parseCategory(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://v3.myafn.dodmedia.osd.mil/api/json/32/channels.json') + .then(r => r.data) + .catch(console.log) + + return data.map(item => ({ + site_id: item.Channel, + name: item.Title + })) + } +} + +function parseStart(item) { + return dayjs.utc(item.e, 'YYYY,M,D,H,m,s,0').add(1, 'month') +} + +function parseCategory(item) { + return item.m ? item.m.split(',') : [] +} + +function parseRating(item) { + return item.j + ? { + system: 'MPA', + value: item.j + } + : null +} + +function parseItems(content, channel) { + const items = JSON.parse(content) + if (!Array.isArray(items)) return [] + + return items.filter(i => i.b == channel.site_id) +} diff --git a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js index 1fefb07c..38e410b7 100644 --- a/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js +++ b/sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.test.js @@ -1,58 +1,58 @@ -// npm run grab -- --site=myafn.dodmedia.osd.mil -// npm run channels:parse -- --config=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js --output=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.channels.xml - -const { parser, url } = require('./myafn.dodmedia.osd.mil.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'AFNPrimeAtlantic.us' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://v3.myafn.dodmedia.osd.mil/api/json/32/2022-10-03@0000/2022-10-03@2359/schedule.json' - ) -}) - -it('can parse response', () => { - const content = - '[{"a":566,"b":2,"c":"2022,9,3,3,0,0,0","d":"2022,9,3,4,0,0,0","e":"2022,9,3,3,0,0,0","f":"2022,9,3,4,0,0,0","g":60,"h":"This Week with George Stephanopoulos (ABC)","i":"Episode Title","j":"TV-14","k":false,"l":"Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show \\"\\"Good Morning America,\\"\\" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.","m":"News,Politics,Public affairs,Talk","n":694284445,"o":60,"p":20,"q":true,"r":694285705,"s":null}]' - const result = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-03T03:00:00.000Z', - stop: '2022-10-03T04:00:00.000Z', - title: 'This Week with George Stephanopoulos (ABC)', - sub_title: 'Episode Title', - description: - 'Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show ""Good Morning America,"" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.', - category: ['News', 'Politics', 'Public affairs', 'Talk'], - rating: { - system: 'MPA', - value: 'TV-14' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: `{ - "Message": "An error has occurred." - }`, - date, - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=myafn.dodmedia.osd.mil +// npm run channels:parse -- --config=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.config.js --output=./sites/myafn.dodmedia.osd.mil/myafn.dodmedia.osd.mil.channels.xml + +const { parser, url } = require('./myafn.dodmedia.osd.mil.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'AFNPrimeAtlantic.us' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://v3.myafn.dodmedia.osd.mil/api/json/32/2022-10-03@0000/2022-10-03@2359/schedule.json' + ) +}) + +it('can parse response', () => { + const content = + '[{"a":566,"b":2,"c":"2022,9,3,3,0,0,0","d":"2022,9,3,4,0,0,0","e":"2022,9,3,3,0,0,0","f":"2022,9,3,4,0,0,0","g":60,"h":"This Week with George Stephanopoulos (ABC)","i":"Episode Title","j":"TV-14","k":false,"l":"Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show \\"\\"Good Morning America,\\"\\" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.","m":"News,Politics,Public affairs,Talk","n":694284445,"o":60,"p":20,"q":true,"r":694285705,"s":null}]' + const result = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-03T03:00:00.000Z', + stop: '2022-10-03T04:00:00.000Z', + title: 'This Week with George Stephanopoulos (ABC)', + sub_title: 'Episode Title', + description: + 'Former Clinton White House staffer and current co-anchor of ABC\'s weekday morning news show ""Good Morning America,"" George Stephanopoulos and co-anchors Martha Raddatz and Jonathan Karl offer a look at current events with a focus on the politics of the day. Each week\'s show includes interviews with top newsmakers (including some of the nation\'s top political leaders) as well as a roundtable discussion, usually featuring journalists from ABC and other news organizations, of the week\'s happenings. Since 2008, the program has broadcast from a studio at the Newseum in Washington, D.C.', + category: ['News', 'Politics', 'Public affairs', 'Talk'], + rating: { + system: 'MPA', + value: 'TV-14' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: `{ + "Message": "An error has occurred." + }`, + date, + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mysky.com.ph/mysky.com.ph.config.js b/sites/mysky.com.ph/mysky.com.ph.config.js index af604cb2..009dacab 100644 --- a/sites/mysky.com.ph/mysky.com.ph.config.js +++ b/sites/mysky.com.ph/mysky.com.ph.config.js @@ -1,62 +1,62 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'mysky.com.ph', - days: 2, - url: 'https://skyepg.mysky.com.ph/Main/getEventsbyType', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.userData.description, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://skyepg.mysky.com.ph/Main/getEventsbyType') - .then(r => r.data.location) - .catch(console.log) - - return items.map(item => ({ - site_id: item.id, - name: item.name - })) - } -} - -function parseStart(item) { - return dayjs.tz(item.start, 'YYYY/MM/DD HH:mm', 'Asia/Manila') -} - -function parseStop(item) { - return dayjs.tz(item.end, 'YYYY/MM/DD HH:mm', 'Asia/Manila') -} - -function parseItems(content, channel, date) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.events)) return [] - const d = date.format('YYYY/MM/DD') - - return data.events.filter(i => i.location == channel.site_id && i.start.includes(d)) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'mysky.com.ph', + days: 2, + url: 'https://skyepg.mysky.com.ph/Main/getEventsbyType', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.userData.description, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://skyepg.mysky.com.ph/Main/getEventsbyType') + .then(r => r.data.location) + .catch(console.log) + + return items.map(item => ({ + site_id: item.id, + name: item.name + })) + } +} + +function parseStart(item) { + return dayjs.tz(item.start, 'YYYY/MM/DD HH:mm', 'Asia/Manila') +} + +function parseStop(item) { + return dayjs.tz(item.end, 'YYYY/MM/DD HH:mm', 'Asia/Manila') +} + +function parseItems(content, channel, date) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.events)) return [] + const d = date.format('YYYY/MM/DD') + + return data.events.filter(i => i.location == channel.site_id && i.start.includes(d)) +} diff --git a/sites/mysky.com.ph/mysky.com.ph.test.js b/sites/mysky.com.ph/mysky.com.ph.test.js index b53c25ae..71063c76 100644 --- a/sites/mysky.com.ph/mysky.com.ph.test.js +++ b/sites/mysky.com.ph/mysky.com.ph.test.js @@ -1,47 +1,47 @@ -// npm run grab -- --site=mysky.com.ph -// npm run channels:parse -- --config=./sites/mysky.com.ph/mysky.com.ph.config.js --output=./sites/mysky.com.ph/mysky.com.ph.channels.xml - -const { parser, url } = require('./mysky.com.ph.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '8', - xmltv_id: 'KapamilyaChannel.ph' -} - -it('can generate valid url', () => { - expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType') -}) - -it('can parse response', () => { - const content = - '{"events":[{"name":"TV PATROL","location":"8","start":"2022/10/04 19:00","end":"2022/10/04 20:00","userData":{"description":"Description example"}},{"name":"DARNA","location":"8","start":"2022/10/05 20:00","end":"2022/10/05 20:45","userData":{"description":""}},{"name":"Zoe Bakes S1","location":"22","start":"2022/10/04 20:30","end":"2022/10/04 21:00","userData":{"description":"Zo Franois Dad is a beekeeper. So for his birthday, she bakes him a special beehiveshaped cake."}}]}' - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-04T11:00:00.000Z', - stop: '2022-10-04T12:00:00.000Z', - title: 'TV PATROL', - description: 'Description example' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel, - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=mysky.com.ph +// npm run channels:parse -- --config=./sites/mysky.com.ph/mysky.com.ph.config.js --output=./sites/mysky.com.ph/mysky.com.ph.channels.xml + +const { parser, url } = require('./mysky.com.ph.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '8', + xmltv_id: 'KapamilyaChannel.ph' +} + +it('can generate valid url', () => { + expect(url).toBe('https://skyepg.mysky.com.ph/Main/getEventsbyType') +}) + +it('can parse response', () => { + const content = + '{"events":[{"name":"TV PATROL","location":"8","start":"2022/10/04 19:00","end":"2022/10/04 20:00","userData":{"description":"Description example"}},{"name":"DARNA","location":"8","start":"2022/10/05 20:00","end":"2022/10/05 20:45","userData":{"description":""}},{"name":"Zoe Bakes S1","location":"22","start":"2022/10/04 20:30","end":"2022/10/04 21:00","userData":{"description":"Zo Franois Dad is a beekeeper. So for his birthday, she bakes him a special beehiveshaped cake."}}]}' + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-04T11:00:00.000Z', + stop: '2022-10-04T12:00:00.000Z', + title: 'TV PATROL', + description: 'Description example' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel, + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/mytvsuper.com/mytvsuper.com.config.js b/sites/mytvsuper.com/mytvsuper.com.config.js index 8a73c0e9..7d3bb17f 100644 --- a/sites/mytvsuper.com/mytvsuper.com.config.js +++ b/sites/mytvsuper.com/mytvsuper.com.config.js @@ -1,82 +1,82 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -const API_ENDPOINT = 'https://content-api.mytvsuper.com/v1' - -module.exports = { - site: 'mytvsuper.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/epg?network_code=${channel.site_id}&from=${date.format( - 'YYYYMMDD' - )}&to=${date.format('YYYYMMDD')}&platform=web` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, date) - for (let item of items) { - const prev = programs[programs.length - 1] - const start = parseStart(item) - const stop = start.add(30, 'm') - if (prev) { - prev.stop = start - } - programs.push({ - title: parseTitle(item, channel), - description: parseDescription(item, channel), - episode: parseInt(item.episode_no), - start: start, - stop: stop - }) - } - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`${API_ENDPOINT}/channel/list?platform=web`) - .then(r => r.data) - .catch(console.error) - - return data.channels.map(c => { - const name = lang === 'en' ? c.name_en : c.name_tc - - return { - site_id: c.network_code, - name, - lang - } - }) - } -} - -function parseTitle(item, channel) { - return channel.lang === 'en' ? item.programme_title_en : item.programme_title_tc -} - -function parseDescription(item, channel) { - return channel.lang === 'en' ? item.episode_synopsis_en : item.episode_synopsis_tc -} - -function parseStart(item) { - return dayjs.tz(item.start_datetime, 'Asia/Hong_Kong') -} - -function parseItems(content, date) { - const data = JSON.parse(content) - if (!Array.isArray(data) || !data.length || !Array.isArray(data[0].item)) return [] - const dayData = data[0].item.find(i => i.date === date.format('YYYY-MM-DD')) - if (!dayData || !Array.isArray(dayData.epg)) return [] - - return dayData.epg -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +const API_ENDPOINT = 'https://content-api.mytvsuper.com/v1' + +module.exports = { + site: 'mytvsuper.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/epg?network_code=${channel.site_id}&from=${date.format( + 'YYYYMMDD' + )}&to=${date.format('YYYYMMDD')}&platform=web` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, date) + for (let item of items) { + const prev = programs[programs.length - 1] + const start = parseStart(item) + const stop = start.add(30, 'm') + if (prev) { + prev.stop = start + } + programs.push({ + title: parseTitle(item, channel), + description: parseDescription(item, channel), + episode: parseInt(item.episode_no), + start: start, + stop: stop + }) + } + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`${API_ENDPOINT}/channel/list?platform=web`) + .then(r => r.data) + .catch(console.error) + + return data.channels.map(c => { + const name = lang === 'en' ? c.name_en : c.name_tc + + return { + site_id: c.network_code, + name, + lang + } + }) + } +} + +function parseTitle(item, channel) { + return channel.lang === 'en' ? item.programme_title_en : item.programme_title_tc +} + +function parseDescription(item, channel) { + return channel.lang === 'en' ? item.episode_synopsis_en : item.episode_synopsis_tc +} + +function parseStart(item) { + return dayjs.tz(item.start_datetime, 'Asia/Hong_Kong') +} + +function parseItems(content, date) { + const data = JSON.parse(content) + if (!Array.isArray(data) || !data.length || !Array.isArray(data[0].item)) return [] + const dayData = data[0].item.find(i => i.date === date.format('YYYY-MM-DD')) + if (!dayData || !Array.isArray(dayData.epg)) return [] + + return dayData.epg +} diff --git a/sites/mytvsuper.com/mytvsuper.com.test.js b/sites/mytvsuper.com/mytvsuper.com.test.js index 6025d492..11b66bf3 100644 --- a/sites/mytvsuper.com/mytvsuper.com.test.js +++ b/sites/mytvsuper.com/mytvsuper.com.test.js @@ -1,71 +1,71 @@ -// npm run channels:parse -- --config=./sites/mytvsuper.com/mytvsuper.com.config.js --output=./sites/mytvsuper.com/mytvsuper.com.channels.xml --set=lang:zh -// npm run grab -- --site=mytvsuper.com - -const { parser, url } = require('./mytvsuper.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'B', - xmltv_id: 'J2.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://content-api.mytvsuper.com/v1/epg?network_code=B&from=20221115&to=20221115&platform=web' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: '想見你#3[粵/普][PG]', - description: - '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。', - episode: 1000003 - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const channelEN = { ...channel, lang: 'en' } - let results = parser({ content, channel: channelEN, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: 'Someday or One Day#3[Can/Man][PG]', - description: 'Description', - episode: 1000003 - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/mytvsuper.com/mytvsuper.com.config.js --output=./sites/mytvsuper.com/mytvsuper.com.channels.xml --set=lang:zh +// npm run grab -- --site=mytvsuper.com + +const { parser, url } = require('./mytvsuper.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'B', + xmltv_id: 'J2.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://content-api.mytvsuper.com/v1/epg?network_code=B&from=20221115&to=20221115&platform=web' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: '想見你#3[粵/普][PG]', + description: + '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。', + episode: 1000003 + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const channelEN = { ...channel, lang: 'en' } + let results = parser({ content, channel: channelEN, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: 'Someday or One Day#3[Can/Man][PG]', + description: 'Description', + episode: 1000003 + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nhk.or.jp/nhk.or.jp.config.js b/sites/nhk.or.jp/nhk.or.jp.config.js index b43bf6a4..a8ed9e35 100644 --- a/sites/nhk.or.jp/nhk.or.jp.config.js +++ b/sites/nhk.or.jp/nhk.or.jp.config.js @@ -1,70 +1,70 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'nhk.or.jp', - days: 5, - output: 'nhk.or.jp.guide.xml', - channels: 'nhk.or.jp.channels.xml', - lang: 'en', - delay: 5000, - - url: function ({ date }) { - return `https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s${date.unix() * 1000}-e${ - date.add(1, 'd').unix() * 1000 - }.json` - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 1000 }, - headers: { - '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' - } - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - - const items = parseItems(context.content) - - items.forEach(item => { - programs.push({ - title: item.title, - start: parseStart(item), - stop: parseStop(item), - description: item.description, - icon: parseIcon(item), - sub_title: item.subtitle - }) - }) - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.channel || !Array.isArray(data.channel.item) ? [] : data.channel.item - } else { - return [] - } -} - -function parseStart(item) { - return dayjs.unix(parseInt(item.pubDate) / 1000) -} - -function parseStop(item) { - return dayjs.unix(parseInt(item.endDate) / 1000) -} - -function parseIcon(item) { - return 'https://www.nhk.or.jp' + item.thumbnail -} +const dayjs = require('dayjs') + +module.exports = { + site: 'nhk.or.jp', + days: 5, + output: 'nhk.or.jp.guide.xml', + channels: 'nhk.or.jp.channels.xml', + lang: 'en', + delay: 5000, + + url: function ({ date }) { + return `https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s${date.unix() * 1000}-e${ + date.add(1, 'd').unix() * 1000 + }.json` + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 1000 }, + headers: { + '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' + } + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + + const items = parseItems(context.content) + + items.forEach(item => { + programs.push({ + title: item.title, + start: parseStart(item), + stop: parseStop(item), + description: item.description, + icon: parseIcon(item), + sub_title: item.subtitle + }) + }) + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.channel || !Array.isArray(data.channel.item) ? [] : data.channel.item + } else { + return [] + } +} + +function parseStart(item) { + return dayjs.unix(parseInt(item.pubDate) / 1000) +} + +function parseStop(item) { + return dayjs.unix(parseInt(item.endDate) / 1000) +} + +function parseIcon(item) { + return 'https://www.nhk.or.jp' + item.thumbnail +} diff --git a/sites/nhk.or.jp/nhk.or.jp.test.js b/sites/nhk.or.jp/nhk.or.jp.test.js index 66d35b0c..b8512c1e 100644 --- a/sites/nhk.or.jp/nhk.or.jp.test.js +++ b/sites/nhk.or.jp/nhk.or.jp.test.js @@ -1,45 +1,45 @@ -// npm run grab -- --site=nhk.or.jp -// npx jest nhk.or.jp.test.js - -const { url, parser } = require('./nhk.or.jp.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-04-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0', - xmltv_id: 'NHKWorldJapan.jp', - lang: 'en', - logo: 'https://www3.nhk.or.jp/nhkworld/common/site_images/nw_webapp_1024x1024.png' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s1682726400000-e1682812800000.json' - ) -}) - -it('can parse response', () => { - const content = - '{"channel":{"item":[{"seriesId":"1007","airingId":"000","title":"NHK NEWSLINE","description":"NHK WORLD-JAPAN\'s flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.","link":"/nhkworld/en/news/","pubDate":"1682726400000","endDate":"1682727000000","vodReserved":false,"jstrm":"1","wstrm":"1","subtitle":"","content":"","content_clean":"","pgm_gr_id":"","thumbnail":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg","thumbnail_s":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_small.jpg","showlist":"0","internal":"0","genre":{"TV":"11","Top":"","LC":""},"vod_id":"","vod_url":"","analytics":"[nhkworld]simul;NHK NEWSLINE;w02,001;1007-000-2023;2023-04-29T09:00:00+09:00"}]}}' - const results = parser({ content }) - - expect(results).toMatchObject([ - { - title: 'NHK NEWSLINE', - start: dayjs(1682726400000), - stop: dayjs(1682727000000), - description: - "NHK WORLD-JAPAN's flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.", - icon: 'https://www.nhk.or.jp/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg', - sub_title: '' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=nhk.or.jp +// npx jest nhk.or.jp.test.js + +const { url, parser } = require('./nhk.or.jp.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-04-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0', + xmltv_id: 'NHKWorldJapan.jp', + lang: 'en', + logo: 'https://www3.nhk.or.jp/nhkworld/common/site_images/nw_webapp_1024x1024.png' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://nwapi.nhk.jp/nhkworld/epg/v7b/world/s1682726400000-e1682812800000.json' + ) +}) + +it('can parse response', () => { + const content = + '{"channel":{"item":[{"seriesId":"1007","airingId":"000","title":"NHK NEWSLINE","description":"NHK WORLD-JAPAN\'s flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.","link":"/nhkworld/en/news/","pubDate":"1682726400000","endDate":"1682727000000","vodReserved":false,"jstrm":"1","wstrm":"1","subtitle":"","content":"","content_clean":"","pgm_gr_id":"","thumbnail":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg","thumbnail_s":"/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_small.jpg","showlist":"0","internal":"0","genre":{"TV":"11","Top":"","LC":""},"vod_id":"","vod_url":"","analytics":"[nhkworld]simul;NHK NEWSLINE;w02,001;1007-000-2023;2023-04-29T09:00:00+09:00"}]}}' + const results = parser({ content }) + + expect(results).toMatchObject([ + { + title: 'NHK NEWSLINE', + start: dayjs(1682726400000), + stop: dayjs(1682727000000), + description: + "NHK WORLD-JAPAN's flagship hourly news program delivers the latest world news, business and weather, with a focus on Japan and the rest of Asia.", + icon: 'https://www.nhk.or.jp/nhkworld/upld/thumbnails/en/tv/regular_program/340aed63308aafd1178172abf6325231_large.jpg', + sub_title: '' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nhkworldpremium.com/nhkworldpremium.com.config.js b/sites/nhkworldpremium.com/nhkworldpremium.com.config.js index 7d64a51d..bdce0c26 100644 --- a/sites/nhkworldpremium.com/nhkworldpremium.com.config.js +++ b/sites/nhkworldpremium.com/nhkworldpremium.com.config.js @@ -1,56 +1,56 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'nhkworldpremium.com', - days: 7, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel }) { - return `https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=${channel.lang}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const start = dayjs.tz(item.schedule, 'Asia/Seoul') - const duration = parseDuration(item) - const stop = start.add(duration, 's') - programs.push({ - title: item.programTitle, - sub_title: item.episodeTitle, - start, - stop - }) - }) - - return programs - } -} - -function parseDuration(item) { - const [h, m, s] = item.period.split(':') - - if (!h || !m || !s) return 0 - - return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s) -} - -function parseItems(content, date) { - try { - const data = JSON.parse(content) - - if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] - - return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) - } catch (err) { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'nhkworldpremium.com', + days: 7, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel }) { + return `https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=${channel.lang}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const start = dayjs.tz(item.schedule, 'Asia/Seoul') + const duration = parseDuration(item) + const stop = start.add(duration, 's') + programs.push({ + title: item.programTitle, + sub_title: item.episodeTitle, + start, + stop + }) + }) + + return programs + } +} + +function parseDuration(item) { + const [h, m, s] = item.period.split(':') + + if (!h || !m || !s) return 0 + + return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s) +} + +function parseItems(content, date) { + try { + const data = JSON.parse(content) + + if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] + + return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) + } catch (err) { + return [] + } +} diff --git a/sites/nhkworldpremium.com/nhkworldpremium.com.test.js b/sites/nhkworldpremium.com/nhkworldpremium.com.test.js index 9b097800..b1a6bf67 100644 --- a/sites/nhkworldpremium.com/nhkworldpremium.com.test.js +++ b/sites/nhkworldpremium.com/nhkworldpremium.com.test.js @@ -1,90 +1,90 @@ -// npm run grab -- --site=nhkworldpremium.com -// npm run grab -- --site=nhkworldpremium.com - -const { parser, url } = require('./nhkworldpremium.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-07-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'NHKWorldPremium.jp', - lang: 'en' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=en') -}) - -it('can generate valid url for Japanese guide', () => { - const channel = { - site_id: '#', - xmltv_id: 'NHKWorldPremium.jp', - lang: 'ja' - } - - expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=ja') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(56) - expect(results[0]).toMatchObject({ - start: '2023-07-09T15:35:00.000Z', - stop: '2023-07-09T16:20:00.000Z', - title: 'NHK Amateur Singing Contest', - sub_title: '"Maizuru City, Kyoto Prefecture"' - }) - - expect(results[55]).toMatchObject({ - start: '2023-07-10T14:35:00.000Z', - stop: '2023-07-10T15:15:00.000Z', - title: 'International News Report 2023', - sub_title: null - }) -}) - -it('can parse response with Japanese guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ja.json')) - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(56) - expect(results[0]).toMatchObject({ - start: '2023-07-09T15:35:00.000Z', - stop: '2023-07-09T16:20:00.000Z', - title: 'NHKのど自慢', - sub_title: '【京都から生放送!▽前川清・相川七瀬】' - }) - - expect(results[55]).toMatchObject({ - start: '2023-07-10T14:35:00.000Z', - stop: '2023-07-10T15:15:00.000Z', - title: '国際報道2023', - sub_title: null - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: {}, date }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=nhkworldpremium.com +// npm run grab -- --site=nhkworldpremium.com + +const { parser, url } = require('./nhkworldpremium.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-07-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'NHKWorldPremium.jp', + lang: 'en' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=en') +}) + +it('can generate valid url for Japanese guide', () => { + const channel = { + site_id: '#', + xmltv_id: 'NHKWorldPremium.jp', + lang: 'ja' + } + + expect(url({ channel })).toBe('https://nhkworldpremium.com/backend/api/v1/front/episodes?lang=ja') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(56) + expect(results[0]).toMatchObject({ + start: '2023-07-09T15:35:00.000Z', + stop: '2023-07-09T16:20:00.000Z', + title: 'NHK Amateur Singing Contest', + sub_title: '"Maizuru City, Kyoto Prefecture"' + }) + + expect(results[55]).toMatchObject({ + start: '2023-07-10T14:35:00.000Z', + stop: '2023-07-10T15:15:00.000Z', + title: 'International News Report 2023', + sub_title: null + }) +}) + +it('can parse response with Japanese guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ja.json')) + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(56) + expect(results[0]).toMatchObject({ + start: '2023-07-09T15:35:00.000Z', + stop: '2023-07-09T16:20:00.000Z', + title: 'NHKのど自慢', + sub_title: '【京都から生放送!▽前川清・相川七瀬】' + }) + + expect(results[55]).toMatchObject({ + start: '2023-07-10T14:35:00.000Z', + stop: '2023-07-10T15:15:00.000Z', + title: '国際報道2023', + sub_title: null + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: {}, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nos.pt/nos.pt.config.js b/sites/nos.pt/nos.pt.config.js index 86c2fc38..14a5a902 100644 --- a/sites/nos.pt/nos.pt.config.js +++ b/sites/nos.pt/nos.pt.config.js @@ -1,114 +1,114 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'nos.pt', - days: 2, - url({ channel }) { - return `https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=${channel.site_id}` - }, - async parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - date = date.subtract(1, 'd') - for (let item of items) { - const $item = cheerio.load(item) - - const channelAcronym = parseChannelAcronym(content) - const programId = parseProgramId($item) - const details = await loadProgramDetails(channelAcronym, programId) - - programs.push({ - title: details.title, - description: details.description, - icon: parseIcon(details), - start: dayjs(details.start), - stop: dayjs(details.stop) - }) - } - - return programs - }, - async channels() { - const html = await axios - .get('https://www.nos.pt/particulares/televisao/guia-tv/Pages/default.aspx') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const items = $('#guide-filters > dl.dropdown-ord > dd > ul > li').toArray() - - return items.map(item => { - const $item = cheerio.load(item) - - return { - lang: 'pt', - site_id: $item('.value').text().trim(), - name: $item('a').clone().children().remove().end().text().trim() - } - }) - } -} - -async function loadProgramDetails(channelAcronym, programId) { - if (!channelAcronym || !programId) return {} - const data = await axios - .post( - 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails', - { - programId, - channelAcronym, - hour: 'undefined', - startHour: 'undefined', - endHour: 'undefined' - }, - { - headers: { - 'content-type': 'application/json; charset=UTF-8' - } - } - ) - .then(r => r.data) - .catch(console.log) - - if (!data) return {} - - const [title, description, image, , , , start, stop] = data.d.split('_#|$_') - - return { - title, - description, - image, - start, - stop - } -} - -function parseIcon(details) { - return details.image ? `https://images.nos.pt/${details.image}` : null -} - -function parseProgramId($item) { - return $item('a').attr('id') -} - -function parseChannelAcronym(content) { - const $ = cheerio.load(content) - - return $('#channel-logo > img').attr('alt') -} - -function parseItems(content, date) { - const day = date.date() - const $ = cheerio.load(content) - - return $(`#day${day} > ul > li`).toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'nos.pt', + days: 2, + url({ channel }) { + return `https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=${channel.site_id}` + }, + async parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + date = date.subtract(1, 'd') + for (let item of items) { + const $item = cheerio.load(item) + + const channelAcronym = parseChannelAcronym(content) + const programId = parseProgramId($item) + const details = await loadProgramDetails(channelAcronym, programId) + + programs.push({ + title: details.title, + description: details.description, + icon: parseIcon(details), + start: dayjs(details.start), + stop: dayjs(details.stop) + }) + } + + return programs + }, + async channels() { + const html = await axios + .get('https://www.nos.pt/particulares/televisao/guia-tv/Pages/default.aspx') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const items = $('#guide-filters > dl.dropdown-ord > dd > ul > li').toArray() + + return items.map(item => { + const $item = cheerio.load(item) + + return { + lang: 'pt', + site_id: $item('.value').text().trim(), + name: $item('a').clone().children().remove().end().text().trim() + } + }) + } +} + +async function loadProgramDetails(channelAcronym, programId) { + if (!channelAcronym || !programId) return {} + const data = await axios + .post( + 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails', + { + programId, + channelAcronym, + hour: 'undefined', + startHour: 'undefined', + endHour: 'undefined' + }, + { + headers: { + 'content-type': 'application/json; charset=UTF-8' + } + } + ) + .then(r => r.data) + .catch(console.log) + + if (!data) return {} + + const [title, description, image, , , , start, stop] = data.d.split('_#|$_') + + return { + title, + description, + image, + start, + stop + } +} + +function parseIcon(details) { + return details.image ? `https://images.nos.pt/${details.image}` : null +} + +function parseProgramId($item) { + return $item('a').attr('id') +} + +function parseChannelAcronym(content) { + const $ = cheerio.load(content) + + return $('#channel-logo > img').attr('alt') +} + +function parseItems(content, date) { + const day = date.date() + const $ = cheerio.load(content) + + return $(`#day${day} > ul > li`).toArray() +} diff --git a/sites/nos.pt/nos.pt.test.js b/sites/nos.pt/nos.pt.test.js index 7ff15af1..3398b08e 100644 --- a/sites/nos.pt/nos.pt.test.js +++ b/sites/nos.pt/nos.pt.test.js @@ -1,100 +1,100 @@ -// npm run channels:parse -- --config=./sites/nos.pt/nos.pt.config.js --output=./sites/nos.pt/nos.pt.channels.xml -// npm run grab -- --site=nos.pt - -const { parser, url } = require('./nos.pt.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5', - xmltv_id: 'RTP1.pt' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe( - 'https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=5' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - - axios.post.mockImplementation((url, data) => { - if ( - url === - 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && - JSON.stringify(data) === - JSON.stringify({ - programId: '81361', - channelAcronym: 'RTP1', - hour: 'undefined', - startHour: 'undefined', - endHour: 'undefined' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_0.json'))) - }) - } else if ( - url === - 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && - JSON.stringify(data) === - JSON.stringify({ - programId: '81382', - channelAcronym: 'RTP1', - hour: 'undefined', - startHour: 'undefined', - endHour: 'undefined' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_21.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-27T23:50:00.000Z', - stop: '2023-01-28T00:36:00.000Z', - title: 'Anatomia de Grey T.17 Ep.3', - description: - 'Os médicos do Grey Sloan continuam a enfrentar a nova realidade do COVID-19 e lidam com um paciente conhecido e teimoso. Koracick fica encarregue dos internos e Link opera um terapeuta sexual.', - icon: 'https://images.nos.pt/b6fd27f4bd0b404abd4c3fc4faa79024_resized_352x198.jpg' - }) - - expect(results[21]).toMatchObject({ - start: '2023-01-28T21:38:00.000Z', - stop: '2023-01-29T00:05:00.000Z', - title: 'MasterChef Portugal T.1 Ep.10', - description: - 'A maior competição de cozinha do mundo arranca ao comando de três dos mais conceituados chefs portugueses: Pedro Pena Bastos, Noélia Jerónimo e Ricardo Costa, que nos vão transmitir os seus conhecimentos e a sua paixão pela cozinha.', - icon: 'https://images.nos.pt/8aa511d697f0401a88a0cb1ec2718cc3_resized_352x198.jpg' - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/nos.pt/nos.pt.config.js --output=./sites/nos.pt/nos.pt.channels.xml +// npm run grab -- --site=nos.pt + +const { parser, url } = require('./nos.pt.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5', + xmltv_id: 'RTP1.pt' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe( + 'https://www.nos.pt/particulares/televisao/guia-tv/Pages/channel.aspx?channel=5' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + + axios.post.mockImplementation((url, data) => { + if ( + url === + 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && + JSON.stringify(data) === + JSON.stringify({ + programId: '81361', + channelAcronym: 'RTP1', + hour: 'undefined', + startHour: 'undefined', + endHour: 'undefined' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_0.json'))) + }) + } else if ( + url === + 'https://www.nos.pt/_layouts/15/Armstrong/ApplicationPages/EPGGetProgramsAndDetails.aspx/GetProgramDetails' && + JSON.stringify(data) === + JSON.stringify({ + programId: '81382', + channelAcronym: 'RTP1', + hour: 'undefined', + startHour: 'undefined', + endHour: 'undefined' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_21.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-27T23:50:00.000Z', + stop: '2023-01-28T00:36:00.000Z', + title: 'Anatomia de Grey T.17 Ep.3', + description: + 'Os médicos do Grey Sloan continuam a enfrentar a nova realidade do COVID-19 e lidam com um paciente conhecido e teimoso. Koracick fica encarregue dos internos e Link opera um terapeuta sexual.', + icon: 'https://images.nos.pt/b6fd27f4bd0b404abd4c3fc4faa79024_resized_352x198.jpg' + }) + + expect(results[21]).toMatchObject({ + start: '2023-01-28T21:38:00.000Z', + stop: '2023-01-29T00:05:00.000Z', + title: 'MasterChef Portugal T.1 Ep.10', + description: + 'A maior competição de cozinha do mundo arranca ao comando de três dos mais conceituados chefs portugueses: Pedro Pena Bastos, Noélia Jerónimo e Ricardo Costa, que nos vão transmitir os seus conhecimentos e a sua paixão pela cozinha.', + icon: 'https://images.nos.pt/8aa511d697f0401a88a0cb1ec2718cc3_resized_352x198.jpg' + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/novacyprus.com/novacyprus.com.config.js b/sites/novacyprus.com/novacyprus.com.config.js index 5096014d..0eb06083 100644 --- a/sites/novacyprus.com/novacyprus.com.config.js +++ b/sites/novacyprus.com/novacyprus.com.config.js @@ -1,67 +1,67 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 - -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'novacyprus.com', - days: 2, - url({ date }) { - return `https://www.novacyprus.com/api/v1/tvprogram/from/${date.format('YYYYMMDD')}/to/${date - .add(1, 'd') - .format('YYYYMMDD')}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(item.slotDuration, 'm') - programs.push({ - title: item.title, - description: item.description, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://www.novacyprus.com/api/v1/guide/dailychannels') - .then(r => r.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'el', - site_id: item.ChannelId, - name: item.nameEl - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.datetime, 'YYYY-MM-DD HH:mm:ss', 'Asia/Nicosia') -} - -function parseIcon(item) { - return item.mediaItems.length ? item.mediaItems[0].CdnUrl : null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.nodes)) return [] - - return data.nodes.filter(i => i.ChannelId === channel.site_id) -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0 + +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'novacyprus.com', + days: 2, + url({ date }) { + return `https://www.novacyprus.com/api/v1/tvprogram/from/${date.format('YYYYMMDD')}/to/${date + .add(1, 'd') + .format('YYYYMMDD')}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(item.slotDuration, 'm') + programs.push({ + title: item.title, + description: item.description, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://www.novacyprus.com/api/v1/guide/dailychannels') + .then(r => r.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'el', + site_id: item.ChannelId, + name: item.nameEl + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.datetime, 'YYYY-MM-DD HH:mm:ss', 'Asia/Nicosia') +} + +function parseIcon(item) { + return item.mediaItems.length ? item.mediaItems[0].CdnUrl : null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.nodes)) return [] + + return data.nodes.filter(i => i.ChannelId === channel.site_id) +} diff --git a/sites/novacyprus.com/novacyprus.com.test.js b/sites/novacyprus.com/novacyprus.com.test.js index 282ce1e3..1e7feee5 100644 --- a/sites/novacyprus.com/novacyprus.com.test.js +++ b/sites/novacyprus.com/novacyprus.com.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/novacyprus.com/novacyprus.com.config.js --output=./sites/novacyprus.com/novacyprus.com.channels.xml -// npm run grab -- --site=novacyprus.com - -const { parser, url } = require('./novacyprus.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '614', - xmltv_id: 'NovaCinema1.gr' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118' - ) -}) - -it('can parse response', () => { - const content = - '{"nodes":[{"datetime":"2021-11-17 06:20:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema1HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000762/d6a2f5e0-dbc0-49c7-9843-e3161ca5ae5d.png","cid":"42","ChannelId":"614","startingTime":"06:20","endTime":"08:10","title":"Δεσμοί Αίματος","description":"Θρίλερ Μυστηρίου","duration":"109","slotDuration":"110","bref":"COMMOBLOOX","mediaItems":[{"MediaListTypeId":"6","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg"},{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_BACKGROUND_CYP.jpg"}]},{"datetime":"2021-11-17 06:00:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema2HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000763/24e05354-d6ad-4949-bcb3-a81d1c1d2cba.png","cid":"62","ChannelId":"653","startingTime":"06:00","endTime":"07:40","title":"Ανυπόφοροι Γείτονες","description":"Κωμωδία","duration":"93","slotDuration":"100","bref":"NEIGHBORSX","mediaItems":[{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_BACKGROUND_CYP.jpg"}]}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-17T04:20:00.000Z', - stop: '2021-11-17T06:10:00.000Z', - title: 'Δεσμοί Αίματος', - description: 'Θρίλερ Μυστηρίου', - icon: 'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"nodes":[],"total":0,"pages":0}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/novacyprus.com/novacyprus.com.config.js --output=./sites/novacyprus.com/novacyprus.com.channels.xml +// npm run grab -- --site=novacyprus.com + +const { parser, url } = require('./novacyprus.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '614', + xmltv_id: 'NovaCinema1.gr' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.novacyprus.com/api/v1/tvprogram/from/20211117/to/20211118' + ) +}) + +it('can parse response', () => { + const content = + '{"nodes":[{"datetime":"2021-11-17 06:20:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema1HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000762/d6a2f5e0-dbc0-49c7-9843-e3161ca5ae5d.png","cid":"42","ChannelId":"614","startingTime":"06:20","endTime":"08:10","title":"Δεσμοί Αίματος","description":"Θρίλερ Μυστηρίου","duration":"109","slotDuration":"110","bref":"COMMOBLOOX","mediaItems":[{"MediaListTypeId":"6","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg"},{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_BACKGROUND_CYP.jpg"}]},{"datetime":"2021-11-17 06:00:00","day":"Wednesday","numDay":17,"numMonth":11,"month":"November","channelName":"Cyprus Novacinema2HD","channelLog":"https://ssl2.novago.gr/EPG/jsp/images/universal/film/logo/20200210/000100/XTV100000763/24e05354-d6ad-4949-bcb3-a81d1c1d2cba.png","cid":"62","ChannelId":"653","startingTime":"06:00","endTime":"07:40","title":"Ανυπόφοροι Γείτονες","description":"Κωμωδία","duration":"93","slotDuration":"100","bref":"NEIGHBORSX","mediaItems":[{"MediaListTypeId":"7","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CROSS.jpg"},{"MediaListTypeId":"8","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_ICON_CYP.jpg"},{"MediaListTypeId":"9","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_POSTER_CYP.jpg"},{"MediaListTypeId":"10","CdnUrl":"http://cache-forthnet.secure.footprint.net/linear/3/1/312582_NEIGHBORSX_BACKGROUND_CYP.jpg"}]}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-17T04:20:00.000Z', + stop: '2021-11-17T06:10:00.000Z', + title: 'Δεσμοί Αίματος', + description: 'Θρίλερ Μυστηρίου', + icon: 'http://cache-forthnet.secure.footprint.net/linear/3/0/305608_COMMOBLOOX_GUIDE_STILL.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"nodes":[],"total":0,"pages":0}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/novasports.gr/novasports.gr.config.js b/sites/novasports.gr/novasports.gr.config.js index 0721f670..5e452c57 100644 --- a/sites/novasports.gr/novasports.gr.config.js +++ b/sites/novasports.gr/novasports.gr.config.js @@ -1,87 +1,87 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'novasports.gr', - days: 2, - url: function ({ date }) { - return `https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - let stop = start.add(30, 'm') - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - stop = stop.add(1, 'd') - } - prev.stop = start - } - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get( - 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' - ) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $( - '#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div' - ).toArray() - return items.map(item => { - const name = $(item).find('.channel').text().trim() - - return { - lang: 'el', - site_id: name, - name - } - }) - } -} - -function parseTitle($item) { - return $item('.title').text().trim() -} - -function parseDescription($item) { - return $item('.subtitle').text().trim() -} - -function parseStart($item, date) { - const timeString = $item('.time').text().trim() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${timeString}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - const $channelElement = $( - `#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div:contains("${channel.site_id}")` - ) - - return $channelElement.find('.channel-program > div').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'novasports.gr', + days: 2, + url: function ({ date }) { + return `https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + let stop = start.add(30, 'm') + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + stop = stop.add(1, 'd') + } + prev.stop = start + } + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get( + 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' + ) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $( + '#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div' + ).toArray() + return items.map(item => { + const name = $(item).find('.channel').text().trim() + + return { + lang: 'el', + site_id: name, + name + } + }) + } +} + +function parseTitle($item) { + return $item('.title').text().trim() +} + +function parseDescription($item) { + return $item('.subtitle').text().trim() +} + +function parseStart($item, date) { + const timeString = $item('.time').text().trim() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${timeString}`, 'YYYY-MM-DD HH:mm', 'Europe/Athens') +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + const $channelElement = $( + `#mc-broadcast-content:nth-child(2) > div > #channelist-slider > div.channelist-wrapper.slider-wrapper.content > div > div:contains("${channel.site_id}")` + ) + + return $channelElement.find('.channel-program > div').toArray() +} diff --git a/sites/novasports.gr/novasports.gr.test.js b/sites/novasports.gr/novasports.gr.test.js index 45f71cc8..0a2c3388 100644 --- a/sites/novasports.gr/novasports.gr.test.js +++ b/sites/novasports.gr/novasports.gr.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/novasports.gr/novasports.gr.config.js --output=./sites/novasports.gr/novasports.gr.channels.xml -// npm run grab -- --site=novasports.gr - -const { parser, url } = require('./novasports.gr.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Novasports Premier League', - xmltv_id: 'NovasportsPremierLeague.gr' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T07:00:00.000Z', - stop: '2022-10-29T07:30:00.000Z', - title: 'Classic Match', - description: 'Τσέλσι - Μάντσεστερ Γ. (1999/00)' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - channel, - date - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/novasports.gr/novasports.gr.config.js --output=./sites/novasports.gr/novasports.gr.channels.xml +// npm run grab -- --site=novasports.gr + +const { parser, url } = require('./novasports.gr.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Novasports Premier League', + xmltv_id: 'NovasportsPremierLeague.gr' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.novasports.gr/wp-admin/admin-ajax.php?action=nova_get_template&template=tv-program/broadcast&dt=2022-10-29' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T07:00:00.000Z', + stop: '2022-10-29T07:30:00.000Z', + title: 'Classic Match', + description: 'Τσέλσι - Μάντσεστερ Γ. (1999/00)' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + channel, + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/nowplayer.now.com/nowplayer.now.com.config.js b/sites/nowplayer.now.com/nowplayer.now.com.config.js index 9cf554ed..84080345 100644 --- a/sites/nowplayer.now.com/nowplayer.now.com.config.js +++ b/sites/nowplayer.now.com/nowplayer.now.com.config.js @@ -1,69 +1,69 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'nowplayer.now.com', - days: 2, - url: function ({ channel, date }) { - const diff = date.diff(dayjs.utc().startOf('d'), 'd') + 1 - - return `https://nowplayer.now.com/tvguide/epglist?channelIdList[]=${channel.site_id}&day=${diff}` - }, - request: { - headers({ channel }) { - return { - Cookie: `LANG=${channel.lang}; Expires=null; Path=/; Domain=nowplayer.now.com` - } - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels({ lang }) { - const html = await axios - .get('https://nowplayer.now.com/channels', { headers: { Accept: 'text/html' } }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(html) - const channels = $('body > div.container > .tv-guide-s-g > div > div').toArray() - - return channels.map(item => { - const $item = cheerio.load(item) - return { - lang, - site_id: $item('.guide-g-play > p.channel').text().replace('CH', ''), - name: $item('.thumbnail > a > span.image > p').text() - } - }) - } -} - -function parseStart(item) { - return dayjs(item.start) -} - -function parseStop(item) { - return dayjs(item.end) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data)) return [] - - return Array.isArray(data[0]) ? data[0] : [] -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'nowplayer.now.com', + days: 2, + url: function ({ channel, date }) { + const diff = date.diff(dayjs.utc().startOf('d'), 'd') + 1 + + return `https://nowplayer.now.com/tvguide/epglist?channelIdList[]=${channel.site_id}&day=${diff}` + }, + request: { + headers({ channel }) { + return { + Cookie: `LANG=${channel.lang}; Expires=null; Path=/; Domain=nowplayer.now.com` + } + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels({ lang }) { + const html = await axios + .get('https://nowplayer.now.com/channels', { headers: { Accept: 'text/html' } }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(html) + const channels = $('body > div.container > .tv-guide-s-g > div > div').toArray() + + return channels.map(item => { + const $item = cheerio.load(item) + return { + lang, + site_id: $item('.guide-g-play > p.channel').text().replace('CH', ''), + name: $item('.thumbnail > a > span.image > p').text() + } + }) + } +} + +function parseStart(item) { + return dayjs(item.start) +} + +function parseStop(item) { + return dayjs(item.end) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data)) return [] + + return Array.isArray(data[0]) ? data[0] : [] +} diff --git a/sites/nowplayer.now.com/nowplayer.now.com.test.js b/sites/nowplayer.now.com/nowplayer.now.com.test.js index da866de9..6f26dbe7 100644 --- a/sites/nowplayer.now.com/nowplayer.now.com.test.js +++ b/sites/nowplayer.now.com/nowplayer.now.com.test.js @@ -1,60 +1,60 @@ -// npm run channels:parse -- --config=./sites/nowplayer.now.com/nowplayer.now.com.config.js --output=./sites/nowplayer.now.com/nowplayer.now.com.channels.xml --set=lang:zh -// npm run grab -- --site=nowplayer.now.com - -const { parser, url, request } = require('./nowplayer.now.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const channel = { - lang: 'zh', - site_id: '096', - xmltv_id: 'ViuTVsix.hk' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ channel, date })).toBe( - 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1' - ) -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ channel, date })).toBe( - 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com' - }) -}) - -it('can parse response', () => { - const content = - '[[{"key":"key_202111174524739","vimProgramId":"202111174524739","name":"ViuTVsix Station Closing","start":1637690400000,"end":1637715600000,"date":"20211124","startTime":"02:00AM","endTime":"09:00AM","duration":420,"recordable":false,"restartTv":false,"npvrProg":false,"npvrStartTime":0,"npvrEndTime":0,"cid":"viutvsix station closing","cc":"","isInWatchlist":false}]]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-23T18:00:00.000Z', - stop: '2021-11-24T01:00:00.000Z', - title: 'ViuTVsix Station Closing' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[[]]' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/nowplayer.now.com/nowplayer.now.com.config.js --output=./sites/nowplayer.now.com/nowplayer.now.com.channels.xml --set=lang:zh +// npm run grab -- --site=nowplayer.now.com + +const { parser, url, request } = require('./nowplayer.now.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const channel = { + lang: 'zh', + site_id: '096', + xmltv_id: 'ViuTVsix.hk' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ channel, date })).toBe( + 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=1' + ) +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ channel, date })).toBe( + 'https://nowplayer.now.com/tvguide/epglist?channelIdList[]=096&day=2' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + Cookie: 'LANG=zh; Expires=null; Path=/; Domain=nowplayer.now.com' + }) +}) + +it('can parse response', () => { + const content = + '[[{"key":"key_202111174524739","vimProgramId":"202111174524739","name":"ViuTVsix Station Closing","start":1637690400000,"end":1637715600000,"date":"20211124","startTime":"02:00AM","endTime":"09:00AM","duration":420,"recordable":false,"restartTv":false,"npvrProg":false,"npvrStartTime":0,"npvrEndTime":0,"cid":"viutvsix station closing","cc":"","isInWatchlist":false}]]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-23T18:00:00.000Z', + stop: '2021-11-24T01:00:00.000Z', + title: 'ViuTVsix Station Closing' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[[]]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js index c337095d..4d002bf3 100644 --- a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js +++ b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js @@ -1,106 +1,106 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.nuevosiglo.com.uy/programacion/getGrilla' - -module.exports = { - site: 'nuevosiglo.com.uy', - days: 2, - url({ date }) { - return `${API_ENDPOINT}?fecha=${date.format('YYYY/MM/DD')}` - }, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - async parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const $item = cheerio.load(item) - const programId = parseProgramId($item) - const details = await loadProgramDetails(programId) - if (!details) continue - programs.push({ - title: details.main_title, - description: details.short_argument, - icon: parseIcon(details), - actors: parseActors(details), - rating: parseRating(details), - date: details.year, - start: parseStart(details), - stop: parseStop(details) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}?fecha=${dayjs().format('YYYY/MM/DD')}`) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(data) - - return $('img') - .map(function () { - return { - lang: 'es', - site_id: $(this).attr('alt').replace('&', '&'), - name: $(this).attr('alt') - } - }) - .get() - } -} - -function parseProgramId($item) { - return $item('*').data('schedule') -} - -function loadProgramDetails(programId) { - return axios - .get(`https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/${programId}`) - .then(r => r.data) - .catch(console.log) -} - -function parseRating(details) { - return details.parental_rating - ? { - system: 'MPAA', - value: details.parental_rating - } - : null -} - -function parseActors(details) { - return details.actors.split(', ') -} - -function parseIcon(details) { - return details.image ? `https://img-ns.s3.amazonaws.com/grid_data/${details.image}` : null -} - -function parseStart(details) { - return dayjs.tz(details.time_start, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseStop(details) { - return dayjs.tz(details.time_end, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - - return $(`img[alt="${channel.site_id}"]`).first().nextUntil('img').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.nuevosiglo.com.uy/programacion/getGrilla' + +module.exports = { + site: 'nuevosiglo.com.uy', + days: 2, + url({ date }) { + return `${API_ENDPOINT}?fecha=${date.format('YYYY/MM/DD')}` + }, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + async parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const $item = cheerio.load(item) + const programId = parseProgramId($item) + const details = await loadProgramDetails(programId) + if (!details) continue + programs.push({ + title: details.main_title, + description: details.short_argument, + icon: parseIcon(details), + actors: parseActors(details), + rating: parseRating(details), + date: details.year, + start: parseStart(details), + stop: parseStop(details) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}?fecha=${dayjs().format('YYYY/MM/DD')}`) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(data) + + return $('img') + .map(function () { + return { + lang: 'es', + site_id: $(this).attr('alt').replace('&', '&'), + name: $(this).attr('alt') + } + }) + .get() + } +} + +function parseProgramId($item) { + return $item('*').data('schedule') +} + +function loadProgramDetails(programId) { + return axios + .get(`https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/${programId}`) + .then(r => r.data) + .catch(console.log) +} + +function parseRating(details) { + return details.parental_rating + ? { + system: 'MPAA', + value: details.parental_rating + } + : null +} + +function parseActors(details) { + return details.actors.split(', ') +} + +function parseIcon(details) { + return details.image ? `https://img-ns.s3.amazonaws.com/grid_data/${details.image}` : null +} + +function parseStart(details) { + return dayjs.tz(details.time_start, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseStop(details) { + return dayjs.tz(details.time_end, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + + return $(`img[alt="${channel.site_id}"]`).first().nextUntil('img').toArray() +} diff --git a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js index 580326bb..ba49859a 100644 --- a/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js +++ b/sites/nuevosiglo.com.uy/nuevosiglo.com.uy.test.js @@ -1,100 +1,100 @@ -// npm run channels:parse -- --config=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js --output=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.channels.xml -// npm run grab -- --site=nuevosiglo.com.uy - -const { parser, url } = require('./nuevosiglo.com.uy.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-02-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'HBO', - xmltv_id: 'HBOLatinAmerica.us' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.nuevosiglo.com.uy/programacion/getGrilla?fecha=2023/02/10' - ) -}) - -it('can parse response', async () => { - axios.get.mockImplementation(url => { - if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769227') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769239') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = await parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-10T01:11:00.000Z', - stop: '2023-02-10T03:46:00.000Z', - title: 'Jurassic World: Dominion', - description: - 'Años después de la destrucción de Isla Nublar, los dinosaurios viven y cazan junto a los humanos. Este equilibrio determinará, si los humanos seguirán siendo los depredadores máximos en un planeta que comparten con las criaturas temibles.', - icon: 'https://img-ns.s3.amazonaws.com/grid_data/23354476.jpg', - date: '2022', - rating: { - system: 'MPAA', - value: 'PG-13' - }, - actors: ['Jeff Goldblum', 'Sam Neill', 'Bryce Dallas Howard'] - }) - - expect(results[1]).toMatchObject({ - start: '2023-02-11T02:06:00.000Z', - stop: '2023-02-11T04:16:00.000Z', - title: 'Black Adam', - description: - 'Black Adam es liberado de su tumba casi cinco mil años después de haber sido encarcelado y recibir sus poderes de los antiguos dioses. Ahora está listo para desatar su forma única de justicia en el mundo.', - icon: 'https://img-ns.s3.amazonaws.com/grid_data/24638423.jpg', - date: '2022', - rating: { - system: 'MPAA', - value: 'PG-13' - }, - actors: [ - 'Aldis Hodge', - 'Dwayne Johnson', - 'Noah Centineo', - 'Sarah Shahi', - 'Marwan Kenzari', - 'Pierce Brosnan', - 'Quintessa Swindell', - 'Mohammed Amer', - 'Bodhi Sabongui', - 'James Cusati-Moyer' - ] - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - channel, - content: '' - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.config.js --output=./sites/nuevosiglo.com.uy/nuevosiglo.com.uy.channels.xml +// npm run grab -- --site=nuevosiglo.com.uy + +const { parser, url } = require('./nuevosiglo.com.uy.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-02-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'HBO', + xmltv_id: 'HBOLatinAmerica.us' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.nuevosiglo.com.uy/programacion/getGrilla?fecha=2023/02/10' + ) +}) + +it('can parse response', async () => { + axios.get.mockImplementation(url => { + if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769227') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://www.nuevosiglo.com.uy/Programacion/getScheduleXId/133769239') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = await parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-10T01:11:00.000Z', + stop: '2023-02-10T03:46:00.000Z', + title: 'Jurassic World: Dominion', + description: + 'Años después de la destrucción de Isla Nublar, los dinosaurios viven y cazan junto a los humanos. Este equilibrio determinará, si los humanos seguirán siendo los depredadores máximos en un planeta que comparten con las criaturas temibles.', + icon: 'https://img-ns.s3.amazonaws.com/grid_data/23354476.jpg', + date: '2022', + rating: { + system: 'MPAA', + value: 'PG-13' + }, + actors: ['Jeff Goldblum', 'Sam Neill', 'Bryce Dallas Howard'] + }) + + expect(results[1]).toMatchObject({ + start: '2023-02-11T02:06:00.000Z', + stop: '2023-02-11T04:16:00.000Z', + title: 'Black Adam', + description: + 'Black Adam es liberado de su tumba casi cinco mil años después de haber sido encarcelado y recibir sus poderes de los antiguos dioses. Ahora está listo para desatar su forma única de justicia en el mundo.', + icon: 'https://img-ns.s3.amazonaws.com/grid_data/24638423.jpg', + date: '2022', + rating: { + system: 'MPAA', + value: 'PG-13' + }, + actors: [ + 'Aldis Hodge', + 'Dwayne Johnson', + 'Noah Centineo', + 'Sarah Shahi', + 'Marwan Kenzari', + 'Pierce Brosnan', + 'Quintessa Swindell', + 'Mohammed Amer', + 'Bodhi Sabongui', + 'James Cusati-Moyer' + ] + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + channel, + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/ontvtonight.com/ontvtonight.com.config.js b/sites/ontvtonight.com/ontvtonight.com.config.js index 673be064..fbe4b74e 100644 --- a/sites/ontvtonight.com/ontvtonight.com.config.js +++ b/sites/ontvtonight.com/ontvtonight.com.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const tz = { - au: 'Australia/Sydney', - ie: 'Europe/Dublin', - uk: 'Europe/London', - us: 'America/New_York' -} - -module.exports = { - site: 'ontvtonight.com', - days: 2, - url: function ({ date, channel }) { - const [region, id] = channel.site_id.split('#') - let url = 'https://www.ontvtonight.com' - if (region) url += `/${region}` - url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}` - - return url - }, - parser: function ({ content, date, channel }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date, channel) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date, channel) { - const [region] = channel.site_id.split('#') - const timezone = region ? tz[region] : tz['uk'] - const timeString = $item('td:nth-child(1) > h5').text().trim() - const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` - - return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezone) -} - -function parseTitle($item) { - return $item('td:nth-child(2) > h5').text().trim() -} - -function parseDescription($item) { - return $item('td:nth-child(2) > h6').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#content > div > div > div.span6 > table > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const tz = { + au: 'Australia/Sydney', + ie: 'Europe/Dublin', + uk: 'Europe/London', + us: 'America/New_York' +} + +module.exports = { + site: 'ontvtonight.com', + days: 2, + url: function ({ date, channel }) { + const [region, id] = channel.site_id.split('#') + let url = 'https://www.ontvtonight.com' + if (region) url += `/${region}` + url += `/guide/listings/channel/${id}.html?dt=${date.format('YYYY-MM-DD')}` + + return url + }, + parser: function ({ content, date, channel }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date, channel) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date, channel) { + const [region] = channel.site_id.split('#') + const timezone = region ? tz[region] : tz['uk'] + const timeString = $item('td:nth-child(1) > h5').text().trim() + const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` + + return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', timezone) +} + +function parseTitle($item) { + return $item('td:nth-child(2) > h5').text().trim() +} + +function parseDescription($item) { + return $item('td:nth-child(2) > h6').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#content > div > div > div.span6 > table > tbody > tr').toArray() +} diff --git a/sites/ontvtonight.com/ontvtonight.com.test.js b/sites/ontvtonight.com/ontvtonight.com.test.js index 70b85b91..ba9ef33e 100644 --- a/sites/ontvtonight.com/ontvtonight.com.test.js +++ b/sites/ontvtonight.com/ontvtonight.com.test.js @@ -1,59 +1,59 @@ -// npm run grab -- --site=ontvtonight.com -// npm run grab -- --site=ontvtonight.com - -const { parser, url } = require('./ontvtonight.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'au#1692/7two', - xmltv_id: '7two.au' -} -const content = - '
    7TWO
    12:10 am
    What A Carry On
    12:50 am
    Bones
    The Devil In The Details
    10:50 pm
    Inspector Morse: The Remorseful Day
    ' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25' - ) -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T13:10:00.000Z', - stop: '2021-11-24T13:50:00.000Z', - title: 'What A Carry On' - }, - { - start: '2021-11-24T13:50:00.000Z', - stop: '2021-11-25T11:50:00.000Z', - title: 'Bones', - description: 'The Devil In The Details' - }, - { - start: '2021-11-25T11:50:00.000Z', - stop: '2021-11-25T12:50:00.000Z', - title: 'Inspector Morse: The Remorseful Day' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=ontvtonight.com +// npm run grab -- --site=ontvtonight.com + +const { parser, url } = require('./ontvtonight.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'au#1692/7two', + xmltv_id: '7two.au' +} +const content = + '
    7TWO
    12:10 am
    What A Carry On
    12:50 am
    Bones
    The Devil In The Details
    10:50 pm
    Inspector Morse: The Remorseful Day
    ' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.ontvtonight.com/au/guide/listings/channel/1692/7two.html?dt=2021-11-25' + ) +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T13:10:00.000Z', + stop: '2021-11-24T13:50:00.000Z', + title: 'What A Carry On' + }, + { + start: '2021-11-24T13:50:00.000Z', + stop: '2021-11-25T11:50:00.000Z', + title: 'Bones', + description: 'The Devil In The Details' + }, + { + start: '2021-11-25T11:50:00.000Z', + stop: '2021-11-25T12:50:00.000Z', + title: 'Inspector Morse: The Remorseful Day' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/osn.com/osn.com.config.js b/sites/osn.com/osn.com.config.js index 955b0756..e1520c8b 100644 --- a/sites/osn.com/osn.com.config.js +++ b/sites/osn.com/osn.com.config.js @@ -1,71 +1,71 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'osn.com', - days: 2, - url: 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Referer: 'https://www.osn.com' - }, - data({ channel, date }) { - return { - newDate: date.format('MM/DD/YYYY'), - selectedCountry: 'AE', - channelCode: channel.site_id, - isMobile: false, - hoursForMobile: 0 - } - }, - jar: null - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item, channel) - const duration = parseDuration(item) - const stop = start.add(duration, 'm') - programs.push({ - title: parseTitle(item, channel), - category: parseCategory(item, channel), - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - } -} - -function parseTitle(item, channel) { - return channel.lang === 'ar' ? item.Arab_Title : item.Title -} - -function parseCategory(item, channel) { - return channel.lang === 'ar' ? item.GenreArabicName : item.GenreEnglishName -} - -function parseDuration(item) { - return parseInt(item.TotalDivWidth / 4.8) -} - -function parseStart(item) { - const time = item.StartDateTime - - return dayjs.tz(time, 'DD MMM YYYY, HH:mm', 'Asia/Dubai') -} - -function parseItems(content) { - if (!content) return [] - const json = JSON.parse(content) - - return json.d ? JSON.parse(json.d) : [] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'osn.com', + days: 2, + url: 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Referer: 'https://www.osn.com' + }, + data({ channel, date }) { + return { + newDate: date.format('MM/DD/YYYY'), + selectedCountry: 'AE', + channelCode: channel.site_id, + isMobile: false, + hoursForMobile: 0 + } + }, + jar: null + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item, channel) + const duration = parseDuration(item) + const stop = start.add(duration, 'm') + programs.push({ + title: parseTitle(item, channel), + category: parseCategory(item, channel), + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + } +} + +function parseTitle(item, channel) { + return channel.lang === 'ar' ? item.Arab_Title : item.Title +} + +function parseCategory(item, channel) { + return channel.lang === 'ar' ? item.GenreArabicName : item.GenreEnglishName +} + +function parseDuration(item) { + return parseInt(item.TotalDivWidth / 4.8) +} + +function parseStart(item) { + const time = item.StartDateTime + + return dayjs.tz(time, 'DD MMM YYYY, HH:mm', 'Asia/Dubai') +} + +function parseItems(content) { + if (!content) return [] + const json = JSON.parse(content) + + return json.d ? JSON.parse(json.d) : [] +} diff --git a/sites/osn.com/osn.com.test.js b/sites/osn.com/osn.com.test.js index d1517d97..48266025 100644 --- a/sites/osn.com/osn.com.test.js +++ b/sites/osn.com/osn.com.test.js @@ -1,69 +1,69 @@ -// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/osn.com/osn.com.config.js --channels=sites/osn.com/osn.com.channels.xml --output=guide.xml --days=2 - -const { parser, url, request } = require('./osn.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channelAR = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'ar' } -const channelEN = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'en' } -const content = JSON.stringify({ - d: '[{"IsPlaying":"0","Durationtime":null,"StartMinute":0,"EndMinute":0,"EmptyDivWidth":1152,"TotalDivWidth":576,"IsTodayDate":false,"IsLastRow":false,"StartDateTime":"24 Oct 2021, 22:00","EndDateTime":"\\/Date(-62135596800000)\\/","Title":"Al Aan TV","Arab_Title":"تلفزيون الآن","GenreEnglishName":null,"GenreArabicName":null,"ChannelNumber":140,"ChannelCode":"AAN","Duration":"\\/Date(-62135596800000)\\/","Showtime":"\\/Date(-62135596800000)\\/","EpisodeId":738257,"ProgramType":null,"EPGUNIQID":"AAN202110271800738257"}]' -}) - -it('can generate valid request data', () => { - const result = request.data({ channel: channelAR, date }) - expect(result).toMatchObject({ - newDate: '10/24/2021', - selectedCountry: 'AE', - channelCode: 'AAN', - isMobile: false, - hoursForMobile: 0 - }) -}) - -it('can generate valid request headers', () => { - const result = request.headers - expect(result).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8', - Referer: 'https://www.osn.com' - }) -}) - -it('can generate valid url', () => { - expect(url).toBe( - 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable' - ) -}) - -it('can parse response (ar)', () => { - const result = parser({ date, channel: channelAR, content }) - expect(result).toMatchObject([ - { - start: 'Sun, 24 Oct 2021 18:00:00 GMT', - stop: 'Sun, 24 Oct 2021 20:00:00 GMT', - title: 'تلفزيون الآن', - category: null - } - ]) -}) - -it('can parse response (en)', () => { - const result = parser({ date, channel: channelEN, content }) - expect(result).toMatchObject([ - { - start: 'Sun, 24 Oct 2021 18:00:00 GMT', - stop: 'Sun, 24 Oct 2021 20:00:00 GMT', - title: 'Al Aan TV', - category: null - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel: channelAR, content: JSON.stringify({ d: '[]' }) }) - expect(result).toMatchObject([]) -}) +// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/osn.com/osn.com.config.js --channels=sites/osn.com/osn.com.channels.xml --output=guide.xml --days=2 + +const { parser, url, request } = require('./osn.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channelAR = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'ar' } +const channelEN = { site_id: 'AAN', xmltv_id: 'AlAanTV.ae', lang: 'en' } +const content = JSON.stringify({ + d: '[{"IsPlaying":"0","Durationtime":null,"StartMinute":0,"EndMinute":0,"EmptyDivWidth":1152,"TotalDivWidth":576,"IsTodayDate":false,"IsLastRow":false,"StartDateTime":"24 Oct 2021, 22:00","EndDateTime":"\\/Date(-62135596800000)\\/","Title":"Al Aan TV","Arab_Title":"تلفزيون الآن","GenreEnglishName":null,"GenreArabicName":null,"ChannelNumber":140,"ChannelCode":"AAN","Duration":"\\/Date(-62135596800000)\\/","Showtime":"\\/Date(-62135596800000)\\/","EpisodeId":738257,"ProgramType":null,"EPGUNIQID":"AAN202110271800738257"}]' +}) + +it('can generate valid request data', () => { + const result = request.data({ channel: channelAR, date }) + expect(result).toMatchObject({ + newDate: '10/24/2021', + selectedCountry: 'AE', + channelCode: 'AAN', + isMobile: false, + hoursForMobile: 0 + }) +}) + +it('can generate valid request headers', () => { + const result = request.headers + expect(result).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8', + Referer: 'https://www.osn.com' + }) +}) + +it('can generate valid url', () => { + expect(url).toBe( + 'https://www.osn.com/CMSPages/TVScheduleWebService.asmx/GetTVChannelsProgramTimeTable' + ) +}) + +it('can parse response (ar)', () => { + const result = parser({ date, channel: channelAR, content }) + expect(result).toMatchObject([ + { + start: 'Sun, 24 Oct 2021 18:00:00 GMT', + stop: 'Sun, 24 Oct 2021 20:00:00 GMT', + title: 'تلفزيون الآن', + category: null + } + ]) +}) + +it('can parse response (en)', () => { + const result = parser({ date, channel: channelEN, content }) + expect(result).toMatchObject([ + { + start: 'Sun, 24 Oct 2021 18:00:00 GMT', + stop: 'Sun, 24 Oct 2021 20:00:00 GMT', + title: 'Al Aan TV', + category: null + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel: channelAR, content: JSON.stringify({ d: '[]' }) }) + expect(result).toMatchObject([]) +}) diff --git a/sites/pbsguam.org/pbsguam.org.config.js b/sites/pbsguam.org/pbsguam.org.config.js index 5f0ce258..6784326c 100644 --- a/sites/pbsguam.org/pbsguam.org.config.js +++ b/sites/pbsguam.org/pbsguam.org.config.js @@ -1,41 +1,41 @@ -const dayjs = require('dayjs') -const isBetween = require('dayjs/plugin/isBetween') - -dayjs.extend(isBetween) - -module.exports = { - site: 'pbsguam.org', - days: 2, // the program is only available Thursday through Sunday - url: 'https://pbsguam.org/calendar/', - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.title, - start: dayjs(item.start), - stop: dayjs(item.end) - }) - }) - - return programs - } -} - -function parseItems(content, date) { - const [, json] = content.match(/EventsSchedule_1 = (.*);/i) || [null, ''] - let data - try { - data = JSON.parse(json) - } catch (error) { - return [] - } - - if (!data || !Array.isArray(data.feed)) return [] - - return data.feed.filter( - i => - dayjs(i.start).isBetween(date, date.add(1, 'd')) || - dayjs(i.end).isBetween(date, date.add(1, 'd')) - ) -} +const dayjs = require('dayjs') +const isBetween = require('dayjs/plugin/isBetween') + +dayjs.extend(isBetween) + +module.exports = { + site: 'pbsguam.org', + days: 2, // the program is only available Thursday through Sunday + url: 'https://pbsguam.org/calendar/', + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.title, + start: dayjs(item.start), + stop: dayjs(item.end) + }) + }) + + return programs + } +} + +function parseItems(content, date) { + const [, json] = content.match(/EventsSchedule_1 = (.*);/i) || [null, ''] + let data + try { + data = JSON.parse(json) + } catch (error) { + return [] + } + + if (!data || !Array.isArray(data.feed)) return [] + + return data.feed.filter( + i => + dayjs(i.start).isBetween(date, date.add(1, 'd')) || + dayjs(i.end).isBetween(date, date.add(1, 'd')) + ) +} diff --git a/sites/pbsguam.org/pbsguam.org.test.js b/sites/pbsguam.org/pbsguam.org.test.js index 732b4f06..5d5b0a9b 100644 --- a/sites/pbsguam.org/pbsguam.org.test.js +++ b/sites/pbsguam.org/pbsguam.org.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=pbsguam.org - -const { parser, url } = require('./pbsguam.org.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'KGTF.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://pbsguam.org/calendar/') -}) - -it('can parse response', () => { - const content = ` ` - const result = parser({ date, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-25T08:30:00.000Z', - stop: '2021-11-25T09:00:00.000Z', - title: 'Xavier Riddle and the Secret Museum' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=pbsguam.org + +const { parser, url } = require('./pbsguam.org.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'KGTF.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://pbsguam.org/calendar/') +}) + +it('can parse response', () => { + const content = ` ` + const result = parser({ date, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-25T08:30:00.000Z', + stop: '2021-11-25T09:00:00.000Z', + title: 'Xavier Riddle and the Secret Museum' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/plex.tv/plex.tv.config.js b/sites/plex.tv/plex.tv.config.js index ac4bd781..72c78425 100644 --- a/sites/plex.tv/plex.tv.config.js +++ b/sites/plex.tv/plex.tv.config.js @@ -1,90 +1,90 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://epg.provider.plex.tv' - -module.exports = { - site: 'plex.tv', - days: 2, - request: { - headers: { - 'x-plex-provider-version': '5.1' - } - }, - url: function ({ channel, date }) { - const [, channelGridKey] = channel.site_id.split('-') - - return `${API_ENDPOINT}/grid?channelGridKey=${channelGridKey}&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - const programs = [] - const items = parseItems(content) - for (let item of items) { - programs.push({ - title: item.title, - description: item.summary, - categories: parseCategories(item), - icon: item.art, - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/lineups/plex/channels?X-Plex-Token=zb85PfdNQLmsry9kQLBR`) - .then(r => r.data) - .catch(console.error) - - return data.MediaContainer.Channel.map(c => { - return { - site_id: c.id, - name: c.title - } - }) - } -} - -function parseCategories(item) { - return Array.isArray(item.Genre) ? item.Genre.map(g => g.tag) : [] -} - -function parseStart(item) { - return item.beginsAt ? dayjs.unix(item.beginsAt) : null -} - -function parseStop(item) { - return item.endsAt ? dayjs.unix(item.endsAt) : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.MediaContainer || !Array.isArray(data.MediaContainer.Metadata)) return [] - const metadata = data.MediaContainer.Metadata - const items = [] - metadata.forEach(item => { - let segments = [] - item.Media.sort(byTime).forEach(media => { - let prevSegment = segments[segments.length - 1] - if (prevSegment && prevSegment.endsAt === media.beginsAt) { - prevSegment.endsAt = media.endsAt - } else { - segments.push(media) - } - }) - - segments.forEach(segment => { - items.push({ ...item, segments, beginsAt: segment.beginsAt, endsAt: segment.endsAt }) - }) - }) - - return items.sort(byTime) - - function byTime(a, b) { - if (a.beginsAt > b.beginsAt) return 1 - if (a.beginsAt < b.beginsAt) return -1 - return 0 - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://epg.provider.plex.tv' + +module.exports = { + site: 'plex.tv', + days: 2, + request: { + headers: { + 'x-plex-provider-version': '5.1' + } + }, + url: function ({ channel, date }) { + const [, channelGridKey] = channel.site_id.split('-') + + return `${API_ENDPOINT}/grid?channelGridKey=${channelGridKey}&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + const programs = [] + const items = parseItems(content) + for (let item of items) { + programs.push({ + title: item.title, + description: item.summary, + categories: parseCategories(item), + icon: item.art, + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/lineups/plex/channels?X-Plex-Token=zb85PfdNQLmsry9kQLBR`) + .then(r => r.data) + .catch(console.error) + + return data.MediaContainer.Channel.map(c => { + return { + site_id: c.id, + name: c.title + } + }) + } +} + +function parseCategories(item) { + return Array.isArray(item.Genre) ? item.Genre.map(g => g.tag) : [] +} + +function parseStart(item) { + return item.beginsAt ? dayjs.unix(item.beginsAt) : null +} + +function parseStop(item) { + return item.endsAt ? dayjs.unix(item.endsAt) : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.MediaContainer || !Array.isArray(data.MediaContainer.Metadata)) return [] + const metadata = data.MediaContainer.Metadata + const items = [] + metadata.forEach(item => { + let segments = [] + item.Media.sort(byTime).forEach(media => { + let prevSegment = segments[segments.length - 1] + if (prevSegment && prevSegment.endsAt === media.beginsAt) { + prevSegment.endsAt = media.endsAt + } else { + segments.push(media) + } + }) + + segments.forEach(segment => { + items.push({ ...item, segments, beginsAt: segment.beginsAt, endsAt: segment.endsAt }) + }) + }) + + return items.sort(byTime) + + function byTime(a, b) { + if (a.beginsAt > b.beginsAt) return 1 + if (a.beginsAt < b.beginsAt) return -1 + return 0 + } +} diff --git a/sites/plex.tv/plex.tv.test.js b/sites/plex.tv/plex.tv.test.js index 6f137e09..ada8e73f 100644 --- a/sites/plex.tv/plex.tv.test.js +++ b/sites/plex.tv/plex.tv.test.js @@ -1,59 +1,59 @@ -// npm run channels:parse -- --config=./sites/plex.tv/plex.tv.config.js --output=./sites/plex.tv/plex.tv.channels.xml -// npm run grab -- --site=plex.tv - -const { parser, url, request } = require('./plex.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-02-05', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', - xmltv_id: 'DarkMatterTV.us' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg.provider.plex.tv/grid?channelGridKey=5eea605674085f0040ddc7a6&date=2023-02-05' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-plex-provider-version': '5.1' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - // expect(results.length).toBe(15) - expect(results[0]).toMatchObject({ - start: '2023-02-04T23:31:14.000Z', - stop: '2023-02-05T01:10:45.000Z', - title: 'Violet & Daisy', - description: - 'Two teenage assassins accept what they think will be a quick-and-easy job, until an unexpected target throws them off their plan.', - icon: 'https://provider-static.plex.tv/epg/images/ott_channels/arts/darkmatter-tv-about.jpg', - categories: ['Movies'] - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/plex.tv/plex.tv.config.js --output=./sites/plex.tv/plex.tv.channels.xml +// npm run grab -- --site=plex.tv + +const { parser, url, request } = require('./plex.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-02-05', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5e20b730f2f8d5003d739db7-5eea605674085f0040ddc7a6', + xmltv_id: 'DarkMatterTV.us' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg.provider.plex.tv/grid?channelGridKey=5eea605674085f0040ddc7a6&date=2023-02-05' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-plex-provider-version': '5.1' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + // expect(results.length).toBe(15) + expect(results[0]).toMatchObject({ + start: '2023-02-04T23:31:14.000Z', + stop: '2023-02-05T01:10:45.000Z', + title: 'Violet & Daisy', + description: + 'Two teenage assassins accept what they think will be a quick-and-easy job, until an unexpected target throws them off their plan.', + icon: 'https://provider-static.plex.tv/epg/images/ott_channels/arts/darkmatter-tv-about.jpg', + categories: ['Movies'] + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js index be1be74a..cf20319e 100644 --- a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js +++ b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js @@ -1,105 +1,105 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'programacion-tv.elpais.com', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date }) { - return `https://programacion-tv.elpais.com/data/parrilla_${date.format('DDMMYYYY')}.json` - }, - parser: async function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - if (!items.length) return programs - const programsData = await loadProgramsData(channel) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - const details = programsData.find(p => p.id_programa === item.id_programa) || {} - programs.push({ - title: item.title, - sub_title: details.episode_title, - description: details.episode_description || item.description, - category: parseCategory(details), - icon: parseIcon(details), - director: parseList(details.director), - actors: parseList(details.actors), - writer: parseList(details.script), - producer: parseList(details.producer), - presenter: parseList(details.presented_by), - composer: parseList(details.music), - guest: parseList(details.guest_actors), - season: parseNumber(details.season), - episode: parseNumber(details.episode), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://programacion-tv.elpais.com/data/canales.json') - .then(r => r.data) - .catch(console.log) - - return Object.values(data).map(item => ({ - lang: 'es', - site_id: item.id, - name: item.nombre - })) - } -} - -function parseNumber(str) { - return typeof str === 'string' ? parseInt(str) : null -} - -function parseList(str) { - return typeof str === 'string' ? str.split(', ') : [] -} - -function parseIcon(details) { - const url = new URL(details.image, 'https://programacion-tv.elpais.com/') - - return url.href -} - -function parseCategory(details) { - return [details.txt_genre, details.txt_subgenre].filter(Boolean).join('/') -} - -async function loadProgramsData(channel) { - return await axios - .get(`https://programacion-tv.elpais.com/data/programas/${channel.site_id}.json`) - .then(r => r.data) - .catch(console.log) -} - -function parseStart(item) { - return dayjs.tz(item.iniDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') -} - -function parseStop(item) { - return dayjs.tz(item.endDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - const channelData = data.find(i => i.idCanal === channel.site_id) - if (!channelData || !Array.isArray(channelData.programas)) return [] - - return channelData.programas -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'programacion-tv.elpais.com', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date }) { + return `https://programacion-tv.elpais.com/data/parrilla_${date.format('DDMMYYYY')}.json` + }, + parser: async function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + if (!items.length) return programs + const programsData = await loadProgramsData(channel) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + const details = programsData.find(p => p.id_programa === item.id_programa) || {} + programs.push({ + title: item.title, + sub_title: details.episode_title, + description: details.episode_description || item.description, + category: parseCategory(details), + icon: parseIcon(details), + director: parseList(details.director), + actors: parseList(details.actors), + writer: parseList(details.script), + producer: parseList(details.producer), + presenter: parseList(details.presented_by), + composer: parseList(details.music), + guest: parseList(details.guest_actors), + season: parseNumber(details.season), + episode: parseNumber(details.episode), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://programacion-tv.elpais.com/data/canales.json') + .then(r => r.data) + .catch(console.log) + + return Object.values(data).map(item => ({ + lang: 'es', + site_id: item.id, + name: item.nombre + })) + } +} + +function parseNumber(str) { + return typeof str === 'string' ? parseInt(str) : null +} + +function parseList(str) { + return typeof str === 'string' ? str.split(', ') : [] +} + +function parseIcon(details) { + const url = new URL(details.image, 'https://programacion-tv.elpais.com/') + + return url.href +} + +function parseCategory(details) { + return [details.txt_genre, details.txt_subgenre].filter(Boolean).join('/') +} + +async function loadProgramsData(channel) { + return await axios + .get(`https://programacion-tv.elpais.com/data/programas/${channel.site_id}.json`) + .then(r => r.data) + .catch(console.log) +} + +function parseStart(item) { + return dayjs.tz(item.iniDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') +} + +function parseStop(item) { + return dayjs.tz(item.endDate, 'YYYY-MM-DD HH:mm:ss', 'Europe/Madrid') +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + const channelData = data.find(i => i.idCanal === channel.site_id) + if (!channelData || !Array.isArray(channelData.programas)) return [] + + return channelData.programas +} diff --git a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js index c600e327..e2001962 100644 --- a/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js +++ b/sites/programacion-tv.elpais.com/programacion-tv.elpais.com.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=programacion-tv.elpais.com -// npm run channels:parse -- --config=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js --output=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.channels.xml - -const { parser, url } = require('./programacion-tv.elpais.com.config.js') -const axios = require('axios') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3', - xmltv_id: 'La1.es' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://programacion-tv.elpais.com/data/parrilla_04102022.json') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - axios.get.mockImplementation(url => { - if (url === 'https://programacion-tv.elpais.com/data/programas/3.json') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/programs.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results).toMatchObject([ - { - start: '2022-10-03T23:30:00.000Z', - stop: '2022-10-04T00:25:00.000Z', - title: 'Comerse el mundo', - sub_title: 'París', - description: - 'El chef Peña viaja hasta París, una de las capitales mundiales de la alta gastronomía. Allí visitará un viñedo muy especial en pleno corazón de la ciudad, probará los famosos caracoles, hará un queso y conocerá a chefs que llegaron a la capital gala para cumplir sus sueños y los consiguieron.', - director: ['Sergio Martín', 'Victor Arribas'], - presenter: ['Javier Peña'], - writer: ['Filippo Gravino', 'Guido Iuculano', 'Michele Pellegrini'], - actors: ['Pietro Sermonti', 'Maya Sansa', 'Ana Caterina Morariu'], - guest: ['Tobia de Angelis', 'Benedetta Porcaroli', 'Roberto Nocchi'], - producer: ['Javier Redondo'], - composer: ['Paco Musulén'], - category: 'Ocio-Cultura/Cocina', - season: 1, - episode: 23, - icon: 'https://programacion-tv.elpais.com/imagenes/programas/2099957.jpg' - } - ]) -}) - -it('can handle empty guide', async () => { - const result = await parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=programacion-tv.elpais.com +// npm run channels:parse -- --config=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.config.js --output=./sites/programacion-tv.elpais.com/programacion-tv.elpais.com.channels.xml + +const { parser, url } = require('./programacion-tv.elpais.com.config.js') +const axios = require('axios') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3', + xmltv_id: 'La1.es' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://programacion-tv.elpais.com/data/parrilla_04102022.json') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + axios.get.mockImplementation(url => { + if (url === 'https://programacion-tv.elpais.com/data/programas/3.json') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/programs.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results).toMatchObject([ + { + start: '2022-10-03T23:30:00.000Z', + stop: '2022-10-04T00:25:00.000Z', + title: 'Comerse el mundo', + sub_title: 'París', + description: + 'El chef Peña viaja hasta París, una de las capitales mundiales de la alta gastronomía. Allí visitará un viñedo muy especial en pleno corazón de la ciudad, probará los famosos caracoles, hará un queso y conocerá a chefs que llegaron a la capital gala para cumplir sus sueños y los consiguieron.', + director: ['Sergio Martín', 'Victor Arribas'], + presenter: ['Javier Peña'], + writer: ['Filippo Gravino', 'Guido Iuculano', 'Michele Pellegrini'], + actors: ['Pietro Sermonti', 'Maya Sansa', 'Ana Caterina Morariu'], + guest: ['Tobia de Angelis', 'Benedetta Porcaroli', 'Roberto Nocchi'], + producer: ['Javier Redondo'], + composer: ['Paco Musulén'], + category: 'Ocio-Cultura/Cocina', + season: 1, + episode: 23, + icon: 'https://programacion-tv.elpais.com/imagenes/programas/2099957.jpg' + } + ]) +}) + +it('can handle empty guide', async () => { + const result = await parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js index 34716713..b72115f1 100644 --- a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js +++ b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js @@ -1,102 +1,102 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/' - -module.exports = { - site: 'programacion.tcc.com.uy', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - maxContentLength: 10 * 1024 * 1024 // 30Mb - }, - url: function ({ date }) { - return `${API_ENDPOINT}?cable_operator=1&emission_start=${date.format( - 'YYYY-MM-DDTHH:mm:ss[Z]' - )}&emission_end=${date.add(1, 'd').format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` - }, - parser({ content, channel }) { - let programs = [] - let items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - categories: parseCategories(item), - date: item.year, - season: item.season_number, - episode: item.episode_number, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get( - `${API_ENDPOINT}?cable_operator=1&emission_start=${dayjs().format( - 'YYYY-MM-DDTHH:mm:ss[Z]' - )}&emission_end=${dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` - ) - .then(r => r.data) - .catch(console.error) - - return data.results.map(c => { - return { - lang: 'es', - site_id: c.id, - name: c.name.replace(/^\[.*\]\s/, '') - } - }) - } -} - -function parseTitle(item) { - const localized = item.localized.find(i => i.language === 'es') - - return localized ? localized.title : item.original_title -} - -function parseDescription(item) { - const localized = item.localized.find(i => i.language === 'es') - - return localized ? localized.description : null -} - -function parseCategories(item) { - return item.genres - .map(g => { - const localized = g.localized.find(i => i.language === 'es') - - return localized ? localized.name : null - }) - .filter(Boolean) -} - -function parseIcon(item) { - const uri = item.images[0] ? item.images[0].image_media.file : null - - return uri ? `https:${uri}` : null -} - -function parseStart(item) { - return dayjs(item.emission_start) -} - -function parseStop(item) { - return dayjs(item.emission_end) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.results)) return [] - const channelData = data.results.find(c => c.id == channel.site_id) - if (!channelData || !Array.isArray(channelData.events)) return [] - - return channelData.events -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/' + +module.exports = { + site: 'programacion.tcc.com.uy', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + maxContentLength: 10 * 1024 * 1024 // 30Mb + }, + url: function ({ date }) { + return `${API_ENDPOINT}?cable_operator=1&emission_start=${date.format( + 'YYYY-MM-DDTHH:mm:ss[Z]' + )}&emission_end=${date.add(1, 'd').format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` + }, + parser({ content, channel }) { + let programs = [] + let items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + categories: parseCategories(item), + date: item.year, + season: item.season_number, + episode: item.episode_number, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get( + `${API_ENDPOINT}?cable_operator=1&emission_start=${dayjs().format( + 'YYYY-MM-DDTHH:mm:ss[Z]' + )}&emission_end=${dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]')}&format=json` + ) + .then(r => r.data) + .catch(console.error) + + return data.results.map(c => { + return { + lang: 'es', + site_id: c.id, + name: c.name.replace(/^\[.*\]\s/, '') + } + }) + } +} + +function parseTitle(item) { + const localized = item.localized.find(i => i.language === 'es') + + return localized ? localized.title : item.original_title +} + +function parseDescription(item) { + const localized = item.localized.find(i => i.language === 'es') + + return localized ? localized.description : null +} + +function parseCategories(item) { + return item.genres + .map(g => { + const localized = g.localized.find(i => i.language === 'es') + + return localized ? localized.name : null + }) + .filter(Boolean) +} + +function parseIcon(item) { + const uri = item.images[0] ? item.images[0].image_media.file : null + + return uri ? `https:${uri}` : null +} + +function parseStart(item) { + return dayjs(item.emission_start) +} + +function parseStop(item) { + return dayjs(item.emission_end) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.results)) return [] + const channelData = data.results.find(c => c.id == channel.site_id) + if (!channelData || !Array.isArray(channelData.events)) return [] + + return channelData.events +} diff --git a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js index cd79c822..1fa7ced0 100644 --- a/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js +++ b/sites/programacion.tcc.com.uy/programacion.tcc.com.uy.test.js @@ -1,79 +1,79 @@ -// npm run channels:parse -- --config=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js --output=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.channels.xml -// npm run grab -- --site=programacion.tcc.com.uy - -const { parser, url } = require('./programacion.tcc.com.uy.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '212', - xmltv_id: 'MultiPremier.mx' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/?cable_operator=1&emission_start=2023-02-11T00:00:00Z&emission_end=2023-02-12T00:00:00Z&format=json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-10T22:45:00.000Z', - stop: '2023-02-11T00:30:00.000Z', - title: 'Meurtres à... - Temp. 3 - Episodio 3', - date: 2016, - season: 3, - episode: 3, - categories: [], - icon: 'https://zpapi.zetatv.com.uy/media/images/2b45d2675389f2e4f7f6fe0655ccc968.jpg', - description: - 'Cada episodio relata un lugar y una historia diferente pero siguiendo la línea de una investigación basada en una leyenda la cual es guiada por una pareja. Estos dos personajes no son necesariamente ambos policías, pero se ven obligados a colaborar a pesar de los primeros informes difíciles.' - }) - expect(results[1]).toMatchObject({ - start: '2023-02-11T00:30:00.000Z', - stop: '2023-02-11T03:00:00.000Z', - title: 'Grandes esperanzas', - date: 1998, - season: null, - episode: null, - categories: ['Drama'], - icon: 'https://zpapi.zetatv.com.uy/media/images/8cab42d88691edaa8a4001b91f809d91.jpg', - description: - 'Basada en la novela de Charles Dickens, cuenta la historia del pintor Finn que persigue obsesionado a su amor de la niñez, la bella y rica Estella. Gracias a un misterioso benefactor, Finn es enviado a Nueva York, donde se reúne con la hermosa y fría joven.' - }) - expect(results[3]).toMatchObject({ - start: '2023-02-11T05:35:00.000Z', - stop: '2023-02-11T07:45:00.000Z', - title: 'Los niños están bien', - date: 2010, - season: null, - episode: null, - categories: ['Comedia', 'Drama'], - icon: 'https://zpapi.zetatv.com.uy/media/images/51684d91ed33cb9b0c1863b7a9b097e9.jpg', - description: - 'Una pareja de lesbianas conciben a un niño y una niña por inseminacion artificial. Al paso del tiempo, los chicos deciden conocer a su verdadero padre a espaldas de sus madres. Tras localizarlo intentan integrar toda una familia. Podran lograrlo?.' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), - channel - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.config.js --output=./sites/programacion.tcc.com.uy/programacion.tcc.com.uy.channels.xml +// npm run grab -- --site=programacion.tcc.com.uy + +const { parser, url } = require('./programacion.tcc.com.uy.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '212', + xmltv_id: 'MultiPremier.mx' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.tccvivo.com.uy/api/v1/navigation_filter/1575/filter/?cable_operator=1&emission_start=2023-02-11T00:00:00Z&emission_end=2023-02-12T00:00:00Z&format=json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-10T22:45:00.000Z', + stop: '2023-02-11T00:30:00.000Z', + title: 'Meurtres à... - Temp. 3 - Episodio 3', + date: 2016, + season: 3, + episode: 3, + categories: [], + icon: 'https://zpapi.zetatv.com.uy/media/images/2b45d2675389f2e4f7f6fe0655ccc968.jpg', + description: + 'Cada episodio relata un lugar y una historia diferente pero siguiendo la línea de una investigación basada en una leyenda la cual es guiada por una pareja. Estos dos personajes no son necesariamente ambos policías, pero se ven obligados a colaborar a pesar de los primeros informes difíciles.' + }) + expect(results[1]).toMatchObject({ + start: '2023-02-11T00:30:00.000Z', + stop: '2023-02-11T03:00:00.000Z', + title: 'Grandes esperanzas', + date: 1998, + season: null, + episode: null, + categories: ['Drama'], + icon: 'https://zpapi.zetatv.com.uy/media/images/8cab42d88691edaa8a4001b91f809d91.jpg', + description: + 'Basada en la novela de Charles Dickens, cuenta la historia del pintor Finn que persigue obsesionado a su amor de la niñez, la bella y rica Estella. Gracias a un misterioso benefactor, Finn es enviado a Nueva York, donde se reúne con la hermosa y fría joven.' + }) + expect(results[3]).toMatchObject({ + start: '2023-02-11T05:35:00.000Z', + stop: '2023-02-11T07:45:00.000Z', + title: 'Los niños están bien', + date: 2010, + season: null, + episode: null, + categories: ['Comedia', 'Drama'], + icon: 'https://zpapi.zetatv.com.uy/media/images/51684d91ed33cb9b0c1863b7a9b097e9.jpg', + description: + 'Una pareja de lesbianas conciben a un niño y una niña por inseminacion artificial. Al paso del tiempo, los chicos deciden conocer a su verdadero padre a espaldas de sus madres. Tras localizarlo intentan integrar toda una familia. Podran lograrlo?.' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')), + channel + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/programetv.ro/programetv.ro.config.js b/sites/programetv.ro/programetv.ro.config.js index 64f73227..c1b3c2ff 100644 --- a/sites/programetv.ro/programetv.ro.config.js +++ b/sites/programetv.ro/programetv.ro.config.js @@ -1,80 +1,80 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'programetv.ro', - days: 2, - url: function ({ date, channel }) { - const daysOfWeek = { - 0: 'duminica', - 1: 'luni', - 2: 'marti', - 3: 'miercuri', - 4: 'joi', - 5: 'vineri', - 6: 'sambata' - } - const day = date.day() - - return `https://www.programetv.ro/post/${channel.site_id}/${daysOfWeek[day]}/` - }, - parser: function ({ content }) { - let programs = [] - const data = parseContent(content) - if (!data || !data.shows) return programs - const items = data.shows - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.titleOriginal, - description: item.desc || item.obs, - category: item.categories, - season: item.season || null, - episode: item.episode || null, - start: parseStart(item), - stop: parseStop(item), - url: item.url || null, - date: item.date, - rating: parseRating(item), - directors: parseDirector(item), - actors: parseActor(item), - icon: item.icon - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs(item.start).toJSON() -} - -function parseStop(item) { - return dayjs(item.stop).toJSON() -} - -function parseContent(content) { - const [, data] = content.match(/var pageData = ((.|[\r\n])+);\n/) || [null, null] - - return data ? JSON.parse(data) : {} -} - -function parseDirector(item) { - return item.credits && item.credits.director ? item.credits.director : null -} - -function parseActor(item) { - return item.credits && item.credits.actor ? item.credits.actor : null -} - -function parseRating(item) { - return item.rating - ? { - system: 'CNC', - value: item.rating.toUpperCase() - } - : null -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'programetv.ro', + days: 2, + url: function ({ date, channel }) { + const daysOfWeek = { + 0: 'duminica', + 1: 'luni', + 2: 'marti', + 3: 'miercuri', + 4: 'joi', + 5: 'vineri', + 6: 'sambata' + } + const day = date.day() + + return `https://www.programetv.ro/post/${channel.site_id}/${daysOfWeek[day]}/` + }, + parser: function ({ content }) { + let programs = [] + const data = parseContent(content) + if (!data || !data.shows) return programs + const items = data.shows + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.titleOriginal, + description: item.desc || item.obs, + category: item.categories, + season: item.season || null, + episode: item.episode || null, + start: parseStart(item), + stop: parseStop(item), + url: item.url || null, + date: item.date, + rating: parseRating(item), + directors: parseDirector(item), + actors: parseActor(item), + icon: item.icon + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs(item.start).toJSON() +} + +function parseStop(item) { + return dayjs(item.stop).toJSON() +} + +function parseContent(content) { + const [, data] = content.match(/var pageData = ((.|[\r\n])+);\n/) || [null, null] + + return data ? JSON.parse(data) : {} +} + +function parseDirector(item) { + return item.credits && item.credits.director ? item.credits.director : null +} + +function parseActor(item) { + return item.credits && item.credits.actor ? item.credits.actor : null +} + +function parseRating(item) { + return item.rating + ? { + system: 'CNC', + value: item.rating.toUpperCase() + } + : null +} diff --git a/sites/programetv.ro/programetv.ro.test.js b/sites/programetv.ro/programetv.ro.test.js index c4e9690e..e0245ec1 100644 --- a/sites/programetv.ro/programetv.ro.test.js +++ b/sites/programetv.ro/programetv.ro.test.js @@ -1,70 +1,70 @@ -// npm run grab -- --site=programetv.ro - -const { parser, url } = require('./programetv.ro.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' } -const content = ` - - - - - -` - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://www.programetv.ro/post/pro-tv/duminica/') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T05:00:00.000Z', - stop: '2021-11-07T07:59:59.000Z', - title: 'Ştirile Pro Tv', - description: - 'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.', - category: ['Ştiri'], - icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA==' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ` - - - - - - -` - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=programetv.ro + +const { parser, url } = require('./programetv.ro.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'pro-tv', xmltv_id: 'ProTV.ro' } +const content = ` + + + + + +` + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://www.programetv.ro/post/pro-tv/duminica/') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T05:00:00.000Z', + stop: '2021-11-07T07:59:59.000Z', + title: 'Ştirile Pro Tv', + description: + 'În fiecare zi, cele mai importante evenimente, transmisiuni LIVE, analize, anchete şi reportaje sunt la Ştirile ProTV.', + category: ['Ştiri'], + icon: 'https://www.programetv.ro/img/shows/84/54/stirile-pro-tv.png?key=Z2lfZnVial90cmFyZXZwLzAwLzAwLzA1LzE4MzgxMnktMTIwazE3MC1hLW40NTk4MW9zLmNhdA==' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ` + + + + + + +` + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programme-tv.net/programme-tv.net.config.js b/sites/programme-tv.net/programme-tv.net.config.js index a566e9c6..5231aa3b 100644 --- a/sites/programme-tv.net/programme-tv.net.config.js +++ b/sites/programme-tv.net/programme-tv.net.config.js @@ -1,73 +1,73 @@ -const durationParser = require('parse-duration') -const cheerio = require('cheerio') -const srcset = require('srcset') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'programme-tv.net', - days: 2, - url: function ({ date, channel }) { - return `https://www.programme-tv.net/programme/chaine/${date.format('YYYY-MM-DD')}/programme-${ - channel.site_id - }.html` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - const icon = parseIcon($item) - const category = parseCategory($item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 'ms') - - programs.push({ title, icon, category, start, stop }) - }) - - return programs - } -} - -function parseStart($item, date) { - let time = $item('.mainBroadcastCard-startingHour').first().text().trim() - time = `${date.format('MM/DD/YYYY')} ${time.replace('h', ':')}` - - return dayjs.tz(time, 'MM/DD/YYYY HH:mm', 'Europe/Paris') -} - -function parseDuration($item) { - const duration = $item('.mainBroadcastCard-durationContent').first().text().trim() - - return durationParser(duration) -} - -function parseIcon($item) { - const img = $item('.mainBroadcastCard-imageContent').first().find('img') - const value = img.attr('srcset') || img.data('srcset') - const obj = value ? srcset.parse(value).find(i => i.width === 128) : {} - - return obj.url -} - -function parseCategory($item) { - return $item('.mainBroadcastCard-genre').first().text().trim() -} - -function parseTitle($item) { - return $item('.mainBroadcastCard-title').first().text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.mainBroadcastCard').toArray() -} +const durationParser = require('parse-duration') +const cheerio = require('cheerio') +const srcset = require('srcset') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'programme-tv.net', + days: 2, + url: function ({ date, channel }) { + return `https://www.programme-tv.net/programme/chaine/${date.format('YYYY-MM-DD')}/programme-${ + channel.site_id + }.html` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + const icon = parseIcon($item) + const category = parseCategory($item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 'ms') + + programs.push({ title, icon, category, start, stop }) + }) + + return programs + } +} + +function parseStart($item, date) { + let time = $item('.mainBroadcastCard-startingHour').first().text().trim() + time = `${date.format('MM/DD/YYYY')} ${time.replace('h', ':')}` + + return dayjs.tz(time, 'MM/DD/YYYY HH:mm', 'Europe/Paris') +} + +function parseDuration($item) { + const duration = $item('.mainBroadcastCard-durationContent').first().text().trim() + + return durationParser(duration) +} + +function parseIcon($item) { + const img = $item('.mainBroadcastCard-imageContent').first().find('img') + const value = img.attr('srcset') || img.data('srcset') + const obj = value ? srcset.parse(value).find(i => i.width === 128) : {} + + return obj.url +} + +function parseCategory($item) { + return $item('.mainBroadcastCard-genre').first().text().trim() +} + +function parseTitle($item) { + return $item('.mainBroadcastCard-title').first().text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.mainBroadcastCard').toArray() +} diff --git a/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js b/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js index c3a9c458..11fcaf96 100644 --- a/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js +++ b/sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js @@ -1,88 +1,88 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -const apiUrl = 'https://programme-tv.vini.pf/programmesJSON' - -module.exports = { - site: 'programme-tv.vini.pf', - days: 2, - url: apiUrl, - request: { - method: 'POST', - data({ date }) { - return { - dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` - } - } - }, - parser: async function ({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - if (items.length) { - for (let hours of [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]) { - const nextContent = await loadNextItems(date, hours) - const nextItems = parseItems(nextContent, channel) - for (let item of nextItems) { - if (!items.find(i => i.nidP === item.nidP)) { - items.push(item) - } - } - } - } - - items.forEach(item => { - programs.push({ - title: item.titreP, - description: item.desc, - category: item.categorieP, - icon: item.srcP, - start: dayjs.unix(item.timestampDeb), - stop: dayjs.unix(item.timestampFin) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post('https://programme-tv.vini.pf/programmesJSON') - .then(r => r.data) - .catch(console.log) - - return data.programmes.map(item => { - const channelId = item.url.replace('/', '') - return { - lang: 'fr', - site_id: channelId, - name: channelId - } - }) - } -} - -async function loadNextItems(date, hours) { - date = date.add(hours, 'h') - - return axios - .post( - apiUrl, - { - dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` - }, - { - responseType: 'arraybuffer' - } - ) - .then(res => res.data.toString()) - .catch(console.log) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.programmes)) return [] - const channelData = data.programmes.find(i => i.url === `/${channel.site_id}`) - if (!channelData) return [] - - return channelData.programmes || [] -} +const dayjs = require('dayjs') +const axios = require('axios') + +const apiUrl = 'https://programme-tv.vini.pf/programmesJSON' + +module.exports = { + site: 'programme-tv.vini.pf', + days: 2, + url: apiUrl, + request: { + method: 'POST', + data({ date }) { + return { + dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` + } + } + }, + parser: async function ({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + if (items.length) { + for (let hours of [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]) { + const nextContent = await loadNextItems(date, hours) + const nextItems = parseItems(nextContent, channel) + for (let item of nextItems) { + if (!items.find(i => i.nidP === item.nidP)) { + items.push(item) + } + } + } + } + + items.forEach(item => { + programs.push({ + title: item.titreP, + description: item.desc, + category: item.categorieP, + icon: item.srcP, + start: dayjs.unix(item.timestampDeb), + stop: dayjs.unix(item.timestampFin) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post('https://programme-tv.vini.pf/programmesJSON') + .then(r => r.data) + .catch(console.log) + + return data.programmes.map(item => { + const channelId = item.url.replace('/', '') + return { + lang: 'fr', + site_id: channelId, + name: channelId + } + }) + } +} + +async function loadNextItems(date, hours) { + date = date.add(hours, 'h') + + return axios + .post( + apiUrl, + { + dateDebut: `${date.subtract(10, 'h').format('YYYY-MM-DDTHH:mm:ss')}-10:00` + }, + { + responseType: 'arraybuffer' + } + ) + .then(res => res.data.toString()) + .catch(console.log) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.programmes)) return [] + const channelData = data.programmes.find(i => i.url === `/${channel.site_id}`) + if (!channelData) return [] + + return channelData.programmes || [] +} diff --git a/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js b/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js index 453fbe6b..46329b4b 100644 --- a/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js +++ b/sites/programme-tv.vini.pf/programme-tv.vini.pf.test.js @@ -1,110 +1,110 @@ -// npm run channels:parse -- --config=./sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js --output=./sites/programme-tv.vini.pf/programme-tv.vini.pf.channels.xml -// npm run grab -- --site=programme-tv.vini.pf - -const { parser, url, request } = require('./programme-tv.vini.pf.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tf1', - xmltv_id: 'TF1.fr' -} - -it('can generate valid url', () => { - expect(url).toBe('https://programme-tv.vini.pf/programmesJSON') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request data', () => { - expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' }) -}) - -it('can parse response', done => { - axios.post.mockImplementation((url, data) => { - if (data.dateDebut === '2021-11-20T16:00:00-10:00') { - return Promise.resolve({ - data: Buffer.from( - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":25,"active":false,"progression":0,"test":0,"nowphp":1637509998},{"nidP":"24162438","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"50mn Inside","heureP":"16:30","timestampDeb":1637461800,"timestampFin":1637466300,"altP":"","titleP":"","legendeP":"L\'actu","desc":"50\'INSIDE, c\'est toute l\'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg","urlP":"/50mn-inside-20112021-1630","width":62.5,"active":false,"progression":0,"test":0,"nowphp":1637509998}]}]}' - ) - }) - } else { - return Promise.resolve({ - data: Buffer.from( - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' - ) - }) - } - }) - - const content = - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162436","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Reportages découverte","heureP":"13:50","timestampDeb":1637452200,"timestampFin":1637457000,"altP":"","titleP":"","legendeP":"La coloc ne connaît pas la crise","desc":"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d\'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg","urlP":"/reportages-decouverte-20112021-1350","width":58.333333333333,"active":false,"progression":0,"test":0,"nowphp":1637509179},{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":41.666666666667,"active":false,"progression":0,"test":0,"nowphp":1637509179}]}]}' - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-20T23:50:00.000Z', - stop: '2021-11-21T01:10:00.000Z', - title: 'Reportages découverte', - category: 'Magazine', - description: - "Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.", - icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg' - }, - { - start: '2021-11-21T01:10:00.000Z', - stop: '2021-11-21T02:30:00.000Z', - title: 'Les docs du week-end', - category: 'Magazine', - description: - 'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?', - icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg' - }, - { - start: '2021-11-21T02:30:00.000Z', - stop: '2021-11-21T03:45:00.000Z', - title: '50mn Inside', - category: 'Magazine', - description: - "50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,", - icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg' - } - ]) - done() - }) - .catch(err => { - done(err) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: - '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(err => { - done(err) - }) -}) +// npm run channels:parse -- --config=./sites/programme-tv.vini.pf/programme-tv.vini.pf.config.js --output=./sites/programme-tv.vini.pf/programme-tv.vini.pf.channels.xml +// npm run grab -- --site=programme-tv.vini.pf + +const { parser, url, request } = require('./programme-tv.vini.pf.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tf1', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + expect(url).toBe('https://programme-tv.vini.pf/programmesJSON') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request data', () => { + expect(request.data({ date })).toMatchObject({ dateDebut: '2021-11-20T14:00:00-10:00' }) +}) + +it('can parse response', done => { + axios.post.mockImplementation((url, data) => { + if (data.dateDebut === '2021-11-20T16:00:00-10:00') { + return Promise.resolve({ + data: Buffer.from( + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":25,"active":false,"progression":0,"test":0,"nowphp":1637509998},{"nidP":"24162438","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"50mn Inside","heureP":"16:30","timestampDeb":1637461800,"timestampFin":1637466300,"altP":"","titleP":"","legendeP":"L\'actu","desc":"50\'INSIDE, c\'est toute l\'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg","urlP":"/50mn-inside-20112021-1630","width":62.5,"active":false,"progression":0,"test":0,"nowphp":1637509998}]}]}' + ) + }) + } else { + return Promise.resolve({ + data: Buffer.from( + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' + ) + }) + } + }) + + const content = + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[{"nidP":"24162436","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Reportages découverte","heureP":"13:50","timestampDeb":1637452200,"timestampFin":1637457000,"altP":"","titleP":"","legendeP":"La coloc ne connaît pas la crise","desc":"Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d\'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg","urlP":"/reportages-decouverte-20112021-1350","width":58.333333333333,"active":false,"progression":0,"test":0,"nowphp":1637509179},{"nidP":"24162437","categorieP":"Magazine","categorieTIDP":"1033","episodeP":"-1","titreP":"Les docs du week-end","heureP":"15:10","timestampDeb":1637457000,"timestampFin":1637461800,"altP":"","titleP":"","legendeP":"Que sont-ils devenus ? L\'incroyable destin des stars des émissions de télécrochet","desc":"Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?","srcP":"https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg","urlP":"/les-docs-du-week-end-20112021-1510","width":41.666666666667,"active":false,"progression":0,"test":0,"nowphp":1637509179}]}]}' + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-20T23:50:00.000Z', + stop: '2021-11-21T01:10:00.000Z', + title: 'Reportages découverte', + category: 'Magazine', + description: + "Pour faire face à la crise du logement, aux loyers toujours plus élevés, à la solitude ou pour les gardes d'enfants, les colocations ont le vent en poupe, Pour mieux comprendre ce nouveau phénomène, une équipe a partagé le quotidien de quatre foyers : une retraitée qui héberge des étudiants, des mamans solos, enceintes, qui partagent un appartement associatif, trois générations de la même famille sur un domaine viticole et une étudiante qui intègre une colocation XXL.", + icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/52ada51ed86b7e7bc11eaee83ff2192785989d77.jpg' + }, + { + start: '2021-11-21T01:10:00.000Z', + stop: '2021-11-21T02:30:00.000Z', + title: 'Les docs du week-end', + category: 'Magazine', + description: + 'Un documentaire français réalisé en 2019, Cindy Sander, Myriam Abel, Mario, Michal ou encore Magali Vaé ont fait les grandes heures des premières émissions de télécrochet modernes, dans les années 2000, Des années après leur passage, que reste-t-il de leur notoriété ? Comment ces candidats ont-ils vécu leur soudaine médiatisation ? Quels rapports entretenaient-ils avec les autres participants et les membres du jury, souvent intransigeants ?', + icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/6e64cfbc55c1f4cbd11e3011401403d4dc08c6d2.jpg' + }, + { + start: '2021-11-21T02:30:00.000Z', + stop: '2021-11-21T03:45:00.000Z', + title: '50mn Inside', + category: 'Magazine', + description: + "50'INSIDE, c'est toute l'actualité des stars résumée, chaque samedi, Le rendez-vous glamour pour retrouver toujours,,", + icon: 'https://programme-tv.vini.pf/sites/default/files/img-icones/3d7e252312dacb5fb7a1a786fa0022ca1be15499.jpg' + } + ]) + done() + }) + .catch(err => { + done(err) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: + '{"programmes":[{"nid":"8857261","src":"https://programme-tv.vini.pf/sites/default/files/img-icones/192.png","alt":"","title":"","url":"/tf1","programmes":[]}]}' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(err => { + done(err) + }) +}) diff --git a/sites/programme.tvb.com/programme.tvb.com.config.js b/sites/programme.tvb.com/programme.tvb.com.config.js index 7fa1d4fe..16aea97a 100644 --- a/sites/programme.tvb.com/programme.tvb.com.config.js +++ b/sites/programme.tvb.com/programme.tvb.com.config.js @@ -1,64 +1,64 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'programme.tvb.com', - days: 2, - url: function ({ channel, date }) { - return `https://programme.tvb.com/ajax.php?action=channellist&code=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.ftit').text().trim() -} - -function parseDescription($item) { - return $item('.full').text().trim() -} - -function parseStart($item, date) { - const time = $item('.time').text() - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD hh:mmA', 'Asia/Hong_Kong') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('ul > li.item').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'programme.tvb.com', + days: 2, + url: function ({ channel, date }) { + return `https://programme.tvb.com/ajax.php?action=channellist&code=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.ftit').text().trim() +} + +function parseDescription($item) { + return $item('.full').text().trim() +} + +function parseStart($item, date) { + const time = $item('.time').text() + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD hh:mmA', 'Asia/Hong_Kong') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('ul > li.item').toArray() +} diff --git a/sites/programme.tvb.com/programme.tvb.com.test.js b/sites/programme.tvb.com/programme.tvb.com.test.js index a17a7b29..18b1e39f 100644 --- a/sites/programme.tvb.com/programme.tvb.com.test.js +++ b/sites/programme.tvb.com/programme.tvb.com.test.js @@ -1,47 +1,47 @@ -// npm run grab -- --site=programme.tvb.com - -const { parser, url } = require('./programme.tvb.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'B', - xmltv_id: 'J2.hk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://programme.tvb.com/ajax.php?action=channellist&code=B&date=2022-11-15' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-14T22:00:00.000Z', - stop: '2022-11-14T23:00:00.000Z', - title: '想見你#3[粵/普][PG]', - description: - '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=programme.tvb.com + +const { parser, url } = require('./programme.tvb.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'B', + xmltv_id: 'J2.hk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://programme.tvb.com/ajax.php?action=channellist&code=B&date=2022-11-15' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-14T22:00:00.000Z', + stop: '2022-11-14T23:00:00.000Z', + title: '想見你#3[粵/普][PG]', + description: + '韻如因父母離婚都不要自己而跑出家門,遇到子維,兩人互吐心事。雨萱順著照片上的唱片行線索,找到一家同名咖啡店,從文磊處得知照片中人是已經過世的韻如,從而推測那個男生也不是詮勝,但她內心反而更加痛苦。' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.html')), + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/programtv.onet.pl/programtv.onet.pl.config.js b/sites/programtv.onet.pl/programtv.onet.pl.config.js index 9028de3c..bffe3ee3 100644 --- a/sites/programtv.onet.pl/programtv.onet.pl.config.js +++ b/sites/programtv.onet.pl/programtv.onet.pl.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - delay: 5000, - site: 'programtv.onet.pl', - days: 2, - url: function ({ date, channel }) { - const currDate = DateTime.now().toUTC().startOf('day') - const day = date.diff(currDate, 'd') - - return `https://programtv.onet.pl/program-tv/${channel.site_id}?dzien=${day}` - }, - parser: function ({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('.hours > .hour').text() - const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Warsaw' }).toUTC() -} - -function parseCategory($item) { - return $item('.titles > .type').text() -} - -function parseDescription($item) { - return $item('.titles > p').text().trim() -} - -function parseTitle($item) { - return $item('.titles > a').text().trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#channelTV > section > div.emissions > ul > li').toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + delay: 5000, + site: 'programtv.onet.pl', + days: 2, + url: function ({ date, channel }) { + const currDate = DateTime.now().toUTC().startOf('day') + const day = date.diff(currDate, 'd') + + return `https://programtv.onet.pl/program-tv/${channel.site_id}?dzien=${day}` + }, + parser: function ({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('.hours > .hour').text() + const dateString = `${date.format('MM/DD/YYYY')} ${timeString}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH:mm', { zone: 'Europe/Warsaw' }).toUTC() +} + +function parseCategory($item) { + return $item('.titles > .type').text() +} + +function parseDescription($item) { + return $item('.titles > p').text().trim() +} + +function parseTitle($item) { + return $item('.titles > a').text().trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#channelTV > section > div.emissions > ul > li').toArray() +} diff --git a/sites/programtv.onet.pl/programtv.onet.pl.test.js b/sites/programtv.onet.pl/programtv.onet.pl.test.js index 1f991e63..dbfbc620 100644 --- a/sites/programtv.onet.pl/programtv.onet.pl.test.js +++ b/sites/programtv.onet.pl/programtv.onet.pl.test.js @@ -1,77 +1,77 @@ -// npm run grab -- --site=programtv.onet.pl - -const MockDate = require('mockdate') -const { parser, url } = require('./programtv.onet.pl.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '13th-street-250', - xmltv_id: '13thStreet.de' -} -const content = - '
    13th Street
    • 03:20
      Law & Order, odc. 15: Letzte Worte Krimiserie

      Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....

    • 23:30
      Navy CIS, odc. 1: New Orleans Krimiserie

      Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...

    • 01:00
      Navy CIS: L.A, odc. 13: High Society Krimiserie

      Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...

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

      Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....

    • 23:30
      Navy CIS, odc. 1: New Orleans Krimiserie

      Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...

    • 01:00
      Navy CIS: L.A, odc. 13: High Society Krimiserie

      Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...

    ' + +it('can generate valid url', () => { + MockDate.set(dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d')) + expect(url({ channel, date })).toBe( + 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=0' + ) + MockDate.reset() +}) + +it('can generate valid url for next day', () => { + MockDate.set(dayjs.utc('2021-11-23', 'YYYY-MM-DD').startOf('d')) + expect(url({ channel, date })).toBe( + 'https://programtv.onet.pl/program-tv/13th-street-250?dzien=1' + ) + MockDate.reset() +}) + +it('can parse response', () => { + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T02:20:00.000Z', + stop: '2021-11-24T22:30:00.000Z', + title: 'Law & Order, odc. 15: Letzte Worte', + category: 'Krimiserie', + description: + 'Bei einer Reality-TV-Show stirbt einer der Teilnehmer. Zunächst tappen Briscoe (Jerry Orbach) und Green (Jesse L....' + }, + { + start: '2021-11-24T22:30:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'Navy CIS, odc. 1: New Orleans', + category: 'Krimiserie', + description: + 'Der Abgeordnete Dan McLane, ein ehemaliger Vorgesetzter von Gibbs, wird in New Orleans ermordet. In den 90er Jahren...' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T01:00:00.000Z', + title: 'Navy CIS: L.A, odc. 13: High Society', + category: 'Krimiserie', + description: + 'Die Zahl der Drogentoten ist gestiegen. Das Team des NCIS glaubt, dass sich Terroristen durch den zunehmenden...' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/proximusmwc.be/proximusmwc.be.config.js b/sites/proximusmwc.be/proximusmwc.be.config.js index d61f01f7..c07899ff 100644 --- a/sites/proximusmwc.be/proximusmwc.be.config.js +++ b/sites/proximusmwc.be/proximusmwc.be.config.js @@ -1,80 +1,80 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'proximusmwc.be', - days: 2, - url: 'https://api.proximusmwc.be/v2/graphql', - request: { - headers: { - 'Content-Type': 'application/json' - }, - data({ channel, date }) { - return { - query: - 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', - variables: { - startTime: date.unix(), - endTime: date.add(1, 'd').unix(), - language: 'fr', - options: { channelIds: [channel.site_id] } - } - } - } - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - icon: parseIcon(item), - category: parseCategory(item), - start: dayjs.unix(item.startTime), - stop: dayjs.unix(item.endTime) - }) - }) - - return programs - }, - async channels() { - const query = { - operationName: 'getPlayableChannels', - variables: { language: 'fr', id: '0' }, - query: - 'query getPlayableChannels($language: String!, $queryParams: ChannelQueryParams, $id: String) { playableChannels(language: $language, queryParams: $queryParams, id: $id) { id name language radio }}' - } - const data = await axios - .post('https://api.proximusmwc.be/v2/graphql', query) - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let item of data.data.playableChannels) { - if (item.radio) continue - channels.push({ - lang: item.language, - site_id: item.id, - name: item.name - }) - } - - return channels - } -} - -function parseCategory(item) { - return item.category ? item.category.replace(/^C\./, '') : null -} - -function parseIcon(item) { - return item.image[0] ? item.image[0].url : null -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.data || !Array.isArray(data.data.schedulesByInterval)) return [] - - return data.data.schedulesByInterval -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'proximusmwc.be', + days: 2, + url: 'https://api.proximusmwc.be/v2/graphql', + request: { + headers: { + 'Content-Type': 'application/json' + }, + data({ channel, date }) { + return { + query: + 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', + variables: { + startTime: date.unix(), + endTime: date.add(1, 'd').unix(), + language: 'fr', + options: { channelIds: [channel.site_id] } + } + } + } + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + icon: parseIcon(item), + category: parseCategory(item), + start: dayjs.unix(item.startTime), + stop: dayjs.unix(item.endTime) + }) + }) + + return programs + }, + async channels() { + const query = { + operationName: 'getPlayableChannels', + variables: { language: 'fr', id: '0' }, + query: + 'query getPlayableChannels($language: String!, $queryParams: ChannelQueryParams, $id: String) { playableChannels(language: $language, queryParams: $queryParams, id: $id) { id name language radio }}' + } + const data = await axios + .post('https://api.proximusmwc.be/v2/graphql', query) + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let item of data.data.playableChannels) { + if (item.radio) continue + channels.push({ + lang: item.language, + site_id: item.id, + name: item.name + }) + } + + return channels + } +} + +function parseCategory(item) { + return item.category ? item.category.replace(/^C\./, '') : null +} + +function parseIcon(item) { + return item.image[0] ? item.image[0].url : null +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.data || !Array.isArray(data.data.schedulesByInterval)) return [] + + return data.data.schedulesByInterval +} diff --git a/sites/proximusmwc.be/proximusmwc.be.test.js b/sites/proximusmwc.be/proximusmwc.be.test.js index 491d8f0f..8cbbb239 100644 --- a/sites/proximusmwc.be/proximusmwc.be.test.js +++ b/sites/proximusmwc.be/proximusmwc.be.test.js @@ -1,69 +1,69 @@ -// npm run channels:parse -- --config=./sites/proximusmwc.be/proximusmwc.be.config.js --output=./sites/proximusmwc.be/proximusmwc.be.channels.xml -// npm run grab -- --site=proximusmwc.be - -const { parser, url, request } = require('./proximusmwc.be.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'UID0024', - xmltv_id: 'DasErste.de' -} - -it('can generate valid url', () => { - expect(url).toBe('https://api.proximusmwc.be/v2/graphql') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - query: - 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', - variables: { - endTime: 1646438400, - language: 'fr', - options: { channelIds: ['UID0024'] }, - startTime: 1646352000 - } - }) -}) - -it('can parse response', () => { - const content = - '{"data":{"schedulesByInterval":[{"trailId":"UID0024_202202225537","programReferenceNumber":"107504040014","channelId":"UID0024","title":"Der Bozen-Krimi","category":"C.Magazine","startTime":1646350800,"endTime":1646356200,"description":"Chiara Schoras alias \\"Capo\\" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.","image":[{"key":"source","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"},{"key":"custom","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/w-%width%_h-%height%/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"}]}]}}' - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-03T23:40:00.000Z', - stop: '2022-03-04T01:10:00.000Z', - title: 'Der Bozen-Krimi', - description: - 'Chiara Schoras alias "Capo" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.', - category: 'Magazine', - icon: 'https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"data":{"schedulesByInterval":[]}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/proximusmwc.be/proximusmwc.be.config.js --output=./sites/proximusmwc.be/proximusmwc.be.channels.xml +// npm run grab -- --site=proximusmwc.be + +const { parser, url, request } = require('./proximusmwc.be.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'UID0024', + xmltv_id: 'DasErste.de' +} + +it('can generate valid url', () => { + expect(url).toBe('https://api.proximusmwc.be/v2/graphql') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + query: + 'query ($language: String!, $startTime: Int!, $endTime: Int!, $options: SchedulesByIntervalOptions) { schedulesByInterval(language: $language, startTime: $startTime, endTime: $endTime, options: $options) { trailId programReferenceNumber channelId title category startTime endTime image { key url __typename } parentalRating detailUrl grouped description shortDescription category categoryId subCategory links { episodeNumber id seasonId seasonName seriesId seriesTitle title type __typename } seriesId __typename }}', + variables: { + endTime: 1646438400, + language: 'fr', + options: { channelIds: ['UID0024'] }, + startTime: 1646352000 + } + }) +}) + +it('can parse response', () => { + const content = + '{"data":{"schedulesByInterval":[{"trailId":"UID0024_202202225537","programReferenceNumber":"107504040014","channelId":"UID0024","title":"Der Bozen-Krimi","category":"C.Magazine","startTime":1646350800,"endTime":1646356200,"description":"Chiara Schoras alias \\"Capo\\" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.","image":[{"key":"source","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"},{"key":"custom","url":"https://experience-cache.proximustv.be:443/posterserver/poster/EPG/w-%width%_h-%height%/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg","__typename":"Image"}]}]}}' + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-03T23:40:00.000Z', + stop: '2022-03-04T01:10:00.000Z', + title: 'Der Bozen-Krimi', + description: + 'Chiara Schoras alias "Capo" Sonja Schwarz muss im 14. Bozen-Krimi nicht nur einen widersprüchlichen Mordfall aufklären, sondern auch ein Geheimnis ans Licht bringen, das zwei Familien auf schmerzhafte Weise untrennbar verbindet.', + category: 'Magazine', + icon: 'https://experience-cache.proximustv.be:443/posterserver/poster/EPG/250_250_BF6BF77FC28F72FA23EAEA6CAAE98B60.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"data":{"schedulesByInterval":[]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/raiplay.it/raiplay.it.config.js b/sites/raiplay.it/raiplay.it.config.js index 803c68b7..27b562d6 100644 --- a/sites/raiplay.it/raiplay.it.config.js +++ b/sites/raiplay.it/raiplay.it.config.js @@ -1,79 +1,79 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'raiplay.it', - days: 2, - url: function ({ date, channel }) { - return `https://www.raiplay.it/palinsesto/app/${channel.site_id}/${date.format( - 'DD-MM-YYYY' - )}.json` - }, - parser: function ({ content, date }) { - const programs = [] - const data = JSON.parse(content) - if (!data.events) return programs - - data.events.forEach(item => { - if (!item.name || !item.hour || !item.duration_in_minutes) return - const start = parseStart(item, date) - const duration = parseInt(item.duration_in_minutes) - const stop = start.add(duration, 'm') - - programs.push({ - title: item.name || item.program.name, - description: item.description, - season: parseSeason(item), - episode: parseEpisode(item), - sub_title: item['episode_title'] || null, - url: parseURL(item), - start, - stop, - icon: parseIcon(item) - }) - }) - - return programs - } -} - -function parseStart(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.hour}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome') -} - -function parseIcon(item) { - let cover = null - if (item.image) { - cover = `https://www.raiplay.it${item.image}` - } - return cover -} - -function parseURL(item) { - let url = null - if (item.weblink) { - url = `https://www.raiplay.it${item.weblink}` - } - if (item.event_weblink) { - url = `https://www.raiplay.it${item.event_weblink}` - } - return url -} - -function parseSeason(item) { - if (!item.season) return null - if (String(item.season).length > 2) return null - return item.season -} - -function parseEpisode(item) { - if (!item.episode) return null - if (String(item.episode).length > 3) return null - return item.episode -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'raiplay.it', + days: 2, + url: function ({ date, channel }) { + return `https://www.raiplay.it/palinsesto/app/${channel.site_id}/${date.format( + 'DD-MM-YYYY' + )}.json` + }, + parser: function ({ content, date }) { + const programs = [] + const data = JSON.parse(content) + if (!data.events) return programs + + data.events.forEach(item => { + if (!item.name || !item.hour || !item.duration_in_minutes) return + const start = parseStart(item, date) + const duration = parseInt(item.duration_in_minutes) + const stop = start.add(duration, 'm') + + programs.push({ + title: item.name || item.program.name, + description: item.description, + season: parseSeason(item), + episode: parseEpisode(item), + sub_title: item['episode_title'] || null, + url: parseURL(item), + start, + stop, + icon: parseIcon(item) + }) + }) + + return programs + } +} + +function parseStart(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.hour}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome') +} + +function parseIcon(item) { + let cover = null + if (item.image) { + cover = `https://www.raiplay.it${item.image}` + } + return cover +} + +function parseURL(item) { + let url = null + if (item.weblink) { + url = `https://www.raiplay.it${item.weblink}` + } + if (item.event_weblink) { + url = `https://www.raiplay.it${item.event_weblink}` + } + return url +} + +function parseSeason(item) { + if (!item.season) return null + if (String(item.season).length > 2) return null + return item.season +} + +function parseEpisode(item) { + if (!item.episode) return null + if (String(item.episode).length > 3) return null + return item.episode +} diff --git a/sites/raiplay.it/raiplay.it.test.js b/sites/raiplay.it/raiplay.it.test.js index a7a2cc4f..3e820074 100644 --- a/sites/raiplay.it/raiplay.it.test.js +++ b/sites/raiplay.it/raiplay.it.test.js @@ -1,51 +1,51 @@ -// npm run grab -- --site=raiplay.it - -const { parser, url } = require('./raiplay.it.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-03', 'YYYY-MM-DD') -const channel = { - site_id: 'rai-2', - xmltv_id: 'Rai2.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json') -}) - -it('can parse response', () => { - const content = - '{ "id": "Page-e120a813-1b92-4057-a214-15943d95aa68", "title": "Pagina Palinsesto", "channel": "Rai 2", "date": "03-05-2022", "events": [ { "id": "ContentItem-2f81030d-803b-456a-9ea5-40233234fd9d", "name": "The Good Doctor S3E5 - La prima volta", "episode_title": "La prima volta", "episode": "5", "season": "3", "description": "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un\'idea geniale, sarà Andrews a portare a termine l\'operazione.", "channel": "Rai 2", "date": "03/05/2022", "hour": "19:40", "duration": "00:50:00", "duration_in_minutes": "50 min", "path_id": "", "weblink": "", "event_weblink": "/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html", "has_video": false, "image": "/dl/img/2020/03/09/1583748471860_dddddd.jpg", "playlist_id": "11430689", "program": { "name": "The Good Doctor", "path_id": "/programmi/thegooddoctor.json", "info_url": "/programmi/info/757edeac-6fff-4dea-afcd-0bcb39f9ea83.json", "weblink": "/programmi/thegooddoctor" } } ], "track_info": { "id": "", "domain": "raiplay", "platform": "[platform]", "media_type": "", "page_type": "", "editor": "raiplay", "year": "2019", "edit_year": "", "section": "guida tv", "sub_section": "rai 2", "content": "guida tv", "title": "", "channel": "", "date": "2019-09-08", "typology": "", "genres": [], "sub_genres": [], "program_title": "", "program_typology": "", "program_genres": [], "program_sub_genres": [], "edition": "", "season": "", "episode_number": "", "episode_title": "", "form": "", "listaDateMo": [], "dfp": {} }}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-03T17:40:00.000Z', - stop: '2022-05-03T18:30:00.000Z', - title: 'The Good Doctor S3E5 - La prima volta', - description: - "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.", - season: '3', - episode: '5', - sub_title: 'La prima volta', - icon: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg', - url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"events":[],"total":0}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=raiplay.it + +const { parser, url } = require('./raiplay.it.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-03', 'YYYY-MM-DD') +const channel = { + site_id: 'rai-2', + xmltv_id: 'Rai2.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.raiplay.it/palinsesto/app/rai-2/03-05-2022.json') +}) + +it('can parse response', () => { + const content = + '{ "id": "Page-e120a813-1b92-4057-a214-15943d95aa68", "title": "Pagina Palinsesto", "channel": "Rai 2", "date": "03-05-2022", "events": [ { "id": "ContentItem-2f81030d-803b-456a-9ea5-40233234fd9d", "name": "The Good Doctor S3E5 - La prima volta", "episode_title": "La prima volta", "episode": "5", "season": "3", "description": "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un\'idea geniale, sarà Andrews a portare a termine l\'operazione.", "channel": "Rai 2", "date": "03/05/2022", "hour": "19:40", "duration": "00:50:00", "duration_in_minutes": "50 min", "path_id": "", "weblink": "", "event_weblink": "/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html", "has_video": false, "image": "/dl/img/2020/03/09/1583748471860_dddddd.jpg", "playlist_id": "11430689", "program": { "name": "The Good Doctor", "path_id": "/programmi/thegooddoctor.json", "info_url": "/programmi/info/757edeac-6fff-4dea-afcd-0bcb39f9ea83.json", "weblink": "/programmi/thegooddoctor" } } ], "track_info": { "id": "", "domain": "raiplay", "platform": "[platform]", "media_type": "", "page_type": "", "editor": "raiplay", "year": "2019", "edit_year": "", "section": "guida tv", "sub_section": "rai 2", "content": "guida tv", "title": "", "channel": "", "date": "2019-09-08", "typology": "", "genres": [], "sub_genres": [], "program_title": "", "program_typology": "", "program_genres": [], "program_sub_genres": [], "edition": "", "season": "", "episode_number": "", "episode_title": "", "form": "", "listaDateMo": [], "dfp": {} }}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-03T17:40:00.000Z', + stop: '2022-05-03T18:30:00.000Z', + title: 'The Good Doctor S3E5 - La prima volta', + description: + "Shaun affronta il suo primo intervento. Il caso si rivela complicato e, nonostante Shaun abbia un'idea geniale, sarà Andrews a portare a termine l'operazione.", + season: '3', + episode: '5', + sub_title: 'La prima volta', + icon: 'https://www.raiplay.it/dl/img/2020/03/09/1583748471860_dddddd.jpg', + url: 'https://www.raiplay.it/dirette/rai2/The-Good-Doctor-S3E5---La-prima-volta-2f81030d-803b-456a-9ea5-40233234fd9d.html' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"events":[],"total":0}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/reportv.com.ar/reportv.com.ar.config.js b/sites/reportv.com.ar/reportv.com.ar.config.js index 974fe891..afc68c68 100644 --- a/sites/reportv.com.ar/reportv.com.ar.config.js +++ b/sites/reportv.com.ar/reportv.com.ar.config.js @@ -1,169 +1,169 @@ -require('dayjs/locale/es') -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const _ = require('lodash') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'reportv.com.ar', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data({ channel, date }) { - const formData = new URLSearchParams() - formData.append('idSenial', channel.site_id) - formData.append('Alineacion', '2694') - formData.append('DiaDesde', date.format('YYYY/MM/DD')) - formData.append('HoraDesde', '00:00:00') - - return formData - } - }, - url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php', - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - for (let item of items) { - const $item = cheerio.load(item) - const start = parseStart($item, date) - const duration = parseDuration($item) - const stop = start.add(duration, 's') - const details = await loadProgramDetails($item) - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - icon: details.icon, - description: details.description, - directors: details.directors, - actors: details.actors, - start, - stop - }) - } - - return programs - }, - async channels() { - const content = await axios - .get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(content) - const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray() - - return items.map(item => { - return { - site_id: $(item).attr('value'), - name: $(item).text() - } - }) - } -} - -async function loadProgramDetails($item) { - const onclick = $item('*').attr('onclick') - const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g - const match = [...onclick.matchAll(regexp)] - const [, id, idc, id_alineacion, idp, title] = match[0] - if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({}) - const formData = new URLSearchParams() - formData.append('id', id) - formData.append('idc', idc) - formData.append('id_alineacion', id_alineacion) - formData.append('idp', idp) - formData.append('title', title) - const content = await axios - .post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData) - .then(r => r.data.toString()) - .catch(console.error) - if (!content) return Promise.resolve({}) - - const $ = cheerio.load(content) - - return Promise.resolve({ - icon: parseIcon($), - actors: parseActors($), - directors: parseDirectors($), - description: parseDescription($) - }) -} - -function parseActors($) { - const section = $('#Ficha > div') - .html() - .split('
    ') - .find(str => str.includes('Actores:')) - if (!section) return null - const $section = cheerio.load(section) - - return $section('span') - .map((i, el) => $(el).text().trim()) - .get() -} - -function parseDirectors($) { - const section = $('#Ficha > div') - .html() - .split('
    ') - .find(str => str.includes('Directores:')) - if (!section) return null - const $section = cheerio.load(section) - - return $section('span') - .map((i, el) => $(el).text().trim()) - .get() -} - -function parseDescription($) { - return $('#Sinopsis > div').text().trim() -} - -function parseIcon($) { - const src = $('#ImgProg').attr('src') - const url = new URL(src, 'https://www.reportv.com.ar/buscador/') - - return url.href -} - -function parseTitle($item) { - const [, title] = $item('div:nth-child(1) > span').text().split(' - ') - - return title -} - -function parseCategory($item) { - return $item('div:nth-child(3) > span').text() -} - -function parseStart($item, date) { - const [time] = $item('div:nth-child(1) > span').text().split(' - ') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas') -} - -function parseDuration($item) { - const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':') - - return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss) -} - -function parseItems(content, date) { - if (!content) return [] - const $ = cheerio.load(content) - const d = _.startCase(date.locale('es').format('DD MMMM YYYY')) - - return $(`.trProg[title*="${d}"]`).toArray() -} +require('dayjs/locale/es') +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const _ = require('lodash') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'reportv.com.ar', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data({ channel, date }) { + const formData = new URLSearchParams() + formData.append('idSenial', channel.site_id) + formData.append('Alineacion', '2694') + formData.append('DiaDesde', date.format('YYYY/MM/DD')) + formData.append('HoraDesde', '00:00:00') + + return formData + } + }, + url: 'https://www.reportv.com.ar/buscador/ProgXSenial.php', + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + for (let item of items) { + const $item = cheerio.load(item) + const start = parseStart($item, date) + const duration = parseDuration($item) + const stop = start.add(duration, 's') + const details = await loadProgramDetails($item) + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + icon: details.icon, + description: details.description, + directors: details.directors, + actors: details.actors, + start, + stop + }) + } + + return programs + }, + async channels() { + const content = await axios + .get('https://www.reportv.com.ar/buscador/Buscador.php?aid=2694') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(content) + const items = $('#tr_home_2 > td:nth-child(1) > select > option').toArray() + + return items.map(item => { + return { + site_id: $(item).attr('value'), + name: $(item).text() + } + }) + } +} + +async function loadProgramDetails($item) { + const onclick = $item('*').attr('onclick') + const regexp = /detallePrograma\((\d+),(\d+),(\d+),(\d+),'([^']+)'\);/g + const match = [...onclick.matchAll(regexp)] + const [, id, idc, id_alineacion, idp, title] = match[0] + if (!id || !idc || !id_alineacion || !idp || !title) return Promise.resolve({}) + const formData = new URLSearchParams() + formData.append('id', id) + formData.append('idc', idc) + formData.append('id_alineacion', id_alineacion) + formData.append('idp', idp) + formData.append('title', title) + const content = await axios + .post('https://www.reportv.com.ar/buscador/DetallePrograma.php', formData) + .then(r => r.data.toString()) + .catch(console.error) + if (!content) return Promise.resolve({}) + + const $ = cheerio.load(content) + + return Promise.resolve({ + icon: parseIcon($), + actors: parseActors($), + directors: parseDirectors($), + description: parseDescription($) + }) +} + +function parseActors($) { + const section = $('#Ficha > div') + .html() + .split('
    ') + .find(str => str.includes('Actores:')) + if (!section) return null + const $section = cheerio.load(section) + + return $section('span') + .map((i, el) => $(el).text().trim()) + .get() +} + +function parseDirectors($) { + const section = $('#Ficha > div') + .html() + .split('
    ') + .find(str => str.includes('Directores:')) + if (!section) return null + const $section = cheerio.load(section) + + return $section('span') + .map((i, el) => $(el).text().trim()) + .get() +} + +function parseDescription($) { + return $('#Sinopsis > div').text().trim() +} + +function parseIcon($) { + const src = $('#ImgProg').attr('src') + const url = new URL(src, 'https://www.reportv.com.ar/buscador/') + + return url.href +} + +function parseTitle($item) { + const [, title] = $item('div:nth-child(1) > span').text().split(' - ') + + return title +} + +function parseCategory($item) { + return $item('div:nth-child(3) > span').text() +} + +function parseStart($item, date) { + const [time] = $item('div:nth-child(1) > span').text().split(' - ') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'America/Caracas') +} + +function parseDuration($item) { + const [hh, mm, ss] = $item('div:nth-child(4) > span').text().split(':') + + return parseInt(hh) * 3600 + parseInt(mm) * 60 + parseInt(ss) +} + +function parseItems(content, date) { + if (!content) return [] + const $ = cheerio.load(content) + const d = _.startCase(date.locale('es').format('DD MMMM YYYY')) + + return $(`.trProg[title*="${d}"]`).toArray() +} diff --git a/sites/reportv.com.ar/reportv.com.ar.test.js b/sites/reportv.com.ar/reportv.com.ar.test.js index 44367883..731b9500 100644 --- a/sites/reportv.com.ar/reportv.com.ar.test.js +++ b/sites/reportv.com.ar/reportv.com.ar.test.js @@ -1,114 +1,114 @@ -// npm run grab -- --site=reportv.com.ar -// npm run channels:parse -- --config=./sites/reportv.com.ar/reportv.com.ar.config.js --output=./sites/reportv.com.ar/reportv.com.ar.channels.xml - -const { parser, url, request } = require('./reportv.com.ar.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) -const axios = require('axios') -jest.mock('axios') - -const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '914', - xmltv_id: 'VePlusVenezuela.ve' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.reportv.com.ar/buscador/ProgXSenial.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('idSenial')).toBe('914') - expect(result.get('Alineacion')).toBe('2694') - expect(result.get('DiaDesde')).toBe('2022/10/03') - expect(result.get('HoraDesde')).toBe('00:00:00') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - axios.post.mockImplementation((url, data) => { - if ( - url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && - data.get('id') == '286096' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) - }) - } else if ( - url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && - data.get('id') == '392803' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) - }) - } else { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/no_program.html')) - }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-03T04:00:00.000Z', - stop: '2022-10-03T05:00:00.000Z', - title: '¿Quién tiene la razón?', - category: 'Talk Show', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/4401882.jpg', - actors: ['Nancy Álvarez'], - description: - 'Espacio que dará de qué hablar cuando la doctora Nancy Álvarez y Carmen Jara, acompañadas de un jurado implacable, lleguen a escuchar y a resolver los problemas de las partes en conflicto para luego decidir quién tiene la razón.' - }) - - expect(results[21]).toMatchObject({ - start: '2022-10-04T03:00:00.000Z', - stop: '2022-10-04T04:00:00.000Z', - title: 'Valeria', - category: 'Comedia', - icon: 'https://www.reportv.com.ar/buscador/img/Programas/18788047.jpg', - directors: ['Inma Torrente'], - actors: [ - 'Diana Gómez', - 'Silma López', - 'Paula Malia', - 'Teresa Riott', - 'Maxi Iglesias', - 'Juanlu González', - 'Aitor Luna', - 'Lauren McFall', - 'Éva Martin', - 'Raquel Ventosa' - ], - description: - 'Valeria es una escritora que no está pasando por su mejor momento a nivel profesional y sentimental. La distancia emocional que la separa de su marido la lleva a refugiarse en sus tres mejores amigas: Carmen, Lola y Nerea. Valeria y sus amigas están inmersas en un torbellino de emociones de amor, amistad, celos, infidelidad, dudas, desamor, secretos, trabajo, preocupaciones, alegrías y sueños sobre el futuro.' - }) -}) - -it('can handle empty guide', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = await parser({ content, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=reportv.com.ar +// npm run channels:parse -- --config=./sites/reportv.com.ar/reportv.com.ar.config.js --output=./sites/reportv.com.ar/reportv.com.ar.channels.xml + +const { parser, url, request } = require('./reportv.com.ar.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) +const axios = require('axios') +jest.mock('axios') + +const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '914', + xmltv_id: 'VePlusVenezuela.ve' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.reportv.com.ar/buscador/ProgXSenial.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('idSenial')).toBe('914') + expect(result.get('Alineacion')).toBe('2694') + expect(result.get('DiaDesde')).toBe('2022/10/03') + expect(result.get('HoraDesde')).toBe('00:00:00') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + axios.post.mockImplementation((url, data) => { + if ( + url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && + data.get('id') == '286096' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program1.html')) + }) + } else if ( + url === 'https://www.reportv.com.ar/buscador/DetallePrograma.php' && + data.get('id') == '392803' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program2.html')) + }) + } else { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/no_program.html')) + }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-03T04:00:00.000Z', + stop: '2022-10-03T05:00:00.000Z', + title: '¿Quién tiene la razón?', + category: 'Talk Show', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/4401882.jpg', + actors: ['Nancy Álvarez'], + description: + 'Espacio que dará de qué hablar cuando la doctora Nancy Álvarez y Carmen Jara, acompañadas de un jurado implacable, lleguen a escuchar y a resolver los problemas de las partes en conflicto para luego decidir quién tiene la razón.' + }) + + expect(results[21]).toMatchObject({ + start: '2022-10-04T03:00:00.000Z', + stop: '2022-10-04T04:00:00.000Z', + title: 'Valeria', + category: 'Comedia', + icon: 'https://www.reportv.com.ar/buscador/img/Programas/18788047.jpg', + directors: ['Inma Torrente'], + actors: [ + 'Diana Gómez', + 'Silma López', + 'Paula Malia', + 'Teresa Riott', + 'Maxi Iglesias', + 'Juanlu González', + 'Aitor Luna', + 'Lauren McFall', + 'Éva Martin', + 'Raquel Ventosa' + ], + description: + 'Valeria es una escritora que no está pasando por su mejor momento a nivel profesional y sentimental. La distancia emocional que la separa de su marido la lleva a refugiarse en sus tres mejores amigas: Carmen, Lola y Nerea. Valeria y sus amigas están inmersas en un torbellino de emociones de amor, amistad, celos, infidelidad, dudas, desamor, secretos, trabajo, preocupaciones, alegrías y sueños sobre el futuro.' + }) +}) + +it('can handle empty guide', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = await parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/rev.bs/rev.bs.config.js b/sites/rev.bs/rev.bs.config.js index 454a02f4..67dcd7bd 100644 --- a/sites/rev.bs/rev.bs.config.js +++ b/sites/rev.bs/rev.bs.config.js @@ -1,68 +1,68 @@ -const _ = require('lodash') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'rev.bs', - days: 2, - url: function ({ date }) { - return `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format('YYYY-MM-DD')}_0.json` - }, - parser: async function ({ content, channel, date }) { - const programs = [] - const items0 = parseItems(content, channel) - if (!items0.length) return programs - const items1 = parseItems(await loadNextItems(date, 1), channel) - const items2 = parseItems(await loadNextItems(date, 2), channel) - const items3 = parseItems(await loadNextItems(date, 3), channel) - const items = _.unionBy(items0, items1, items2, items3, 'sid') - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.duration, 'm') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -async function loadNextItems(date, index) { - const url = `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format( - 'YYYY-MM-DD' - )}_${index}.json` - - return axios - .get(url, { - responseType: 'arraybuffer' - }) - .then(res => res.data.toString()) - .catch(console.log) -} - -function parseStart(item, date) { - const shift = parseInt(item.s) - - return dayjs.tz(date.add(shift, 'm').toString(), 'America/New_York') -} - -function parseItems(content, channel) { - let data - try { - data = JSON.parse(content) - } catch (error) { - return [] - } - - if (!data || data.status !== 'OK') return [] - - return data.data.schedule[channel.site_id] || [] -} +const _ = require('lodash') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'rev.bs', + days: 2, + url: function ({ date }) { + return `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format('YYYY-MM-DD')}_0.json` + }, + parser: async function ({ content, channel, date }) { + const programs = [] + const items0 = parseItems(content, channel) + if (!items0.length) return programs + const items1 = parseItems(await loadNextItems(date, 1), channel) + const items2 = parseItems(await loadNextItems(date, 2), channel) + const items3 = parseItems(await loadNextItems(date, 3), channel) + const items = _.unionBy(items0, items1, items2, items3, 'sid') + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.duration, 'm') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +async function loadNextItems(date, index) { + const url = `https://www.rev.bs/wp-content/uploads/tv-guide/${date.format( + 'YYYY-MM-DD' + )}_${index}.json` + + return axios + .get(url, { + responseType: 'arraybuffer' + }) + .then(res => res.data.toString()) + .catch(console.log) +} + +function parseStart(item, date) { + const shift = parseInt(item.s) + + return dayjs.tz(date.add(shift, 'm').toString(), 'America/New_York') +} + +function parseItems(content, channel) { + let data + try { + data = JSON.parse(content) + } catch (error) { + return [] + } + + if (!data || data.status !== 'OK') return [] + + return data.data.schedule[channel.site_id] || [] +} diff --git a/sites/rev.bs/rev.bs.test.js b/sites/rev.bs/rev.bs.test.js index 96897def..d03b261b 100644 --- a/sites/rev.bs/rev.bs.test.js +++ b/sites/rev.bs/rev.bs.test.js @@ -1,83 +1,83 @@ -// npm run channels:parse -- --config=./sites/rev.bs/rev.bs.config.js --output=./sites/rev.bs/rev.bs.channels.xml -// npm run grab -- --site=rev.bs - -const { parser, url } = require('./rev.bs.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '206', - xmltv_id: 'WTVJ.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_0.json' - ) -}) - -it('can parse response', done => { - axios.get.mockImplementation(url => { - if (url === 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_1.json') { - return Promise.resolve({ - data: Buffer.from( - '{"status":"OK","data":{"schedule":{"206":[{"title":"Talk Stoop","sid":43599836,"s":"330.0000","duration":30,"rating":"TVPG"}]}}}' - ) - }) - } else { - return Promise.resolve({ - data: Buffer.from('{"status":"OK","data":{"schedule":{}}}') - }) - } - }) - - const content = - '{"status":"OK","data":{"schedule":{"205":[{"title":"Rev Pulse 5 - Online Classifieds","sid":43576112,"s":"-120.0000","duration":120,"rating":""}],"206":[{"title":"Saturday Night Live","sid":43599827,"s":"-31.0000","duration":93,"rating":"TV14"}]}}}' - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-21T04:29:00.000Z', - stop: '2021-11-21T06:02:00.000Z', - title: 'Saturday Night Live' - }, - { - start: '2021-11-21T10:30:00.000Z', - stop: '2021-11-21T11:00:00.000Z', - title: 'Talk Stoop' - } - ]) - done() - }) - .catch(err => { - done(err) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: '' - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(err => { - done(err) - }) -}) +// npm run channels:parse -- --config=./sites/rev.bs/rev.bs.config.js --output=./sites/rev.bs/rev.bs.channels.xml +// npm run grab -- --site=rev.bs + +const { parser, url } = require('./rev.bs.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '206', + xmltv_id: 'WTVJ.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_0.json' + ) +}) + +it('can parse response', done => { + axios.get.mockImplementation(url => { + if (url === 'https://www.rev.bs/wp-content/uploads/tv-guide/2021-11-21_1.json') { + return Promise.resolve({ + data: Buffer.from( + '{"status":"OK","data":{"schedule":{"206":[{"title":"Talk Stoop","sid":43599836,"s":"330.0000","duration":30,"rating":"TVPG"}]}}}' + ) + }) + } else { + return Promise.resolve({ + data: Buffer.from('{"status":"OK","data":{"schedule":{}}}') + }) + } + }) + + const content = + '{"status":"OK","data":{"schedule":{"205":[{"title":"Rev Pulse 5 - Online Classifieds","sid":43576112,"s":"-120.0000","duration":120,"rating":""}],"206":[{"title":"Saturday Night Live","sid":43599827,"s":"-31.0000","duration":93,"rating":"TV14"}]}}}' + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-21T04:29:00.000Z', + stop: '2021-11-21T06:02:00.000Z', + title: 'Saturday Night Live' + }, + { + start: '2021-11-21T10:30:00.000Z', + stop: '2021-11-21T11:00:00.000Z', + title: 'Talk Stoop' + } + ]) + done() + }) + .catch(err => { + done(err) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: '' + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(err => { + done(err) + }) +}) diff --git a/sites/rotana.net/rotana.net.config.js b/sites/rotana.net/rotana.net.config.js index 32e5e973..6ad86acb 100644 --- a/sites/rotana.net/rotana.net.config.js +++ b/sites/rotana.net/rotana.net.config.js @@ -1,66 +1,66 @@ -const stream = require('stream') -const csv = require('csv-parser') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rotana.net', - days: 2, - url({ channel }) { - return `https://rotana.net/triAssets/uploads/2020/11/${channel.site_id}.csv` - }, - request: { - method: 'POST' - }, - parser: async function ({ buffer, date }) { - let programs = [] - const items = await parseItems(buffer, date) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title: item['Arabic Event Name'], - category: item['Genre'], - description: item['Arabic Extended Description'], - start: start.toJSON(), - stop: stop.toJSON() - }) - }) - - return programs - } -} - -function parseStart(item) { - const time = `${item['Start Date']} ${item['Start Time']}` - - return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') -} - -function parseStop(item) { - const time = `${item['End Date']} ${item['End Time']}` - - return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') -} - -function parseItems(buffer, date) { - return new Promise(resolve => { - let items = [] - const input = new stream.PassThrough() - input.end(buffer) - input - .pipe(csv()) - .on('data', data => items.push(data)) - .on('end', () => { - items = items.filter(i => i['Start Date'] === date.format('DD/MM/YYYY')) - resolve(items) - }) - .on('error', () => { - resolve([]) - }) - }) -} +const stream = require('stream') +const csv = require('csv-parser') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rotana.net', + days: 2, + url({ channel }) { + return `https://rotana.net/triAssets/uploads/2020/11/${channel.site_id}.csv` + }, + request: { + method: 'POST' + }, + parser: async function ({ buffer, date }) { + let programs = [] + const items = await parseItems(buffer, date) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title: item['Arabic Event Name'], + category: item['Genre'], + description: item['Arabic Extended Description'], + start: start.toJSON(), + stop: stop.toJSON() + }) + }) + + return programs + } +} + +function parseStart(item) { + const time = `${item['Start Date']} ${item['Start Time']}` + + return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') +} + +function parseStop(item) { + const time = `${item['End Date']} ${item['End Time']}` + + return dayjs.utc(time, 'DD/MM/YYYY HH:mm:ss:00') +} + +function parseItems(buffer, date) { + return new Promise(resolve => { + let items = [] + const input = new stream.PassThrough() + input.end(buffer) + input + .pipe(csv()) + .on('data', data => items.push(data)) + .on('end', () => { + items = items.filter(i => i['Start Date'] === date.format('DD/MM/YYYY')) + resolve(items) + }) + .on('error', () => { + resolve([]) + }) + }) +} diff --git a/sites/rotana.net/rotana.net.test.js b/sites/rotana.net/rotana.net.test.js index a10fe57f..a006b383 100644 --- a/sites/rotana.net/rotana.net.test.js +++ b/sites/rotana.net/rotana.net.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=rotana.net - -const { parser, url } = require('./rotana.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'KHALIJIA-7', - xmltv_id: 'RotanaKhalejia.sa' -} -const buffer = - Buffer.from(`Event ID,Event Name,Arabic Event Name,Start Date,Start Time,End Date,End Time,Short Description,Arabic Short Description,Extended Description,Arabic Extended Description,,Genre,Audio,Video -,حسب الظروف,حسب الظروف بدون تترات - Episode 16,07/11/2021,23:30:00:00,08/11/2021,00:00:00:00,,,,,,Drama,, -,كورة,كورة,08/11/2021,01:30:00:00,08/11/2021,03:00:00:00,,,,,,Generic,,`) - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe('https://rotana.net/triAssets/uploads/2020/11/KHALIJIA-7.csv') -}) - -it('can parse response', done => { - parser({ date, channel, buffer }) - .then(result => { - expect(result).toMatchObject([ - { - start: '2021-11-08T01:30:00.000Z', - stop: '2021-11-08T03:00:00.000Z', - title: 'كورة', - category: 'Generic', - description: '' - } - ]) - done() - }) - .catch(() => { - done() - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - buffer: Buffer.from('') - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(() => { - done() - }) -}) +// npm run grab -- --site=rotana.net + +const { parser, url } = require('./rotana.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'KHALIJIA-7', + xmltv_id: 'RotanaKhalejia.sa' +} +const buffer = + Buffer.from(`Event ID,Event Name,Arabic Event Name,Start Date,Start Time,End Date,End Time,Short Description,Arabic Short Description,Extended Description,Arabic Extended Description,,Genre,Audio,Video +,حسب الظروف,حسب الظروف بدون تترات - Episode 16,07/11/2021,23:30:00:00,08/11/2021,00:00:00:00,,,,,,Drama,, +,كورة,كورة,08/11/2021,01:30:00:00,08/11/2021,03:00:00:00,,,,,,Generic,,`) + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe('https://rotana.net/triAssets/uploads/2020/11/KHALIJIA-7.csv') +}) + +it('can parse response', done => { + parser({ date, channel, buffer }) + .then(result => { + expect(result).toMatchObject([ + { + start: '2021-11-08T01:30:00.000Z', + stop: '2021-11-08T03:00:00.000Z', + title: 'كورة', + category: 'Generic', + description: '' + } + ]) + done() + }) + .catch(() => { + done() + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + buffer: Buffer.from('') + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(() => { + done() + }) +}) diff --git a/sites/rtb.gov.bn/rtb.gov.bn.config.js b/sites/rtb.gov.bn/rtb.gov.bn.config.js index 306ceef8..3af095de 100644 --- a/sites/rtb.gov.bn/rtb.gov.bn.config.js +++ b/sites/rtb.gov.bn/rtb.gov.bn.config.js @@ -1,74 +1,74 @@ -const pdf = require('pdf-parse') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rtb.gov.bn', - days: 2, - url: function ({ channel, date }) { - return encodeURI( - `http://www.rtb.gov.bn/PublishingImages/SitePages/Programme Guide/${ - channel.site_id - } ${date.format('DD MMMM YYYY')}.pdf` - ) - }, - parser: async function ({ buffer, date }) { - let programs = [] - const items = await parseItems(buffer) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(1, 'h') - programs.push({ - title: item.title, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Brunei') -} - -async function parseItems(buffer) { - let data - try { - data = await pdf(buffer) - } catch (err) { - return [] - } - - if (!data) return [] - - return data.text - .split('\n') - .filter(s => { - const string = s.trim() - - return string && /^\d{2}:\d{2}/.test(string) - }) - .map(s => { - const [, time, title] = s.trim().match(/^(\d{2}:\d{2}) (.*)/) || [null, null, null] - - return { time, title } - }) -} +const pdf = require('pdf-parse') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rtb.gov.bn', + days: 2, + url: function ({ channel, date }) { + return encodeURI( + `http://www.rtb.gov.bn/PublishingImages/SitePages/Programme Guide/${ + channel.site_id + } ${date.format('DD MMMM YYYY')}.pdf` + ) + }, + parser: async function ({ buffer, date }) { + let programs = [] + const items = await parseItems(buffer) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(1, 'h') + programs.push({ + title: item.title, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Asia/Brunei') +} + +async function parseItems(buffer) { + let data + try { + data = await pdf(buffer) + } catch (err) { + return [] + } + + if (!data) return [] + + return data.text + .split('\n') + .filter(s => { + const string = s.trim() + + return string && /^\d{2}:\d{2}/.test(string) + }) + .map(s => { + const [, time, title] = s.trim().match(/^(\d{2}:\d{2}) (.*)/) || [null, null, null] + + return { time, title } + }) +} diff --git a/sites/rtb.gov.bn/rtb.gov.bn.test.js b/sites/rtb.gov.bn/rtb.gov.bn.test.js index b3f1ca75..d539504a 100644 --- a/sites/rtb.gov.bn/rtb.gov.bn.test.js +++ b/sites/rtb.gov.bn/rtb.gov.bn.test.js @@ -1,96 +1,96 @@ -// npm run grab -- --site=rtb.gov.bn - -const { parser, url } = require('./rtb.gov.bn.config.js') -const path = require('path') -const fs = require('fs') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Sukmaindera', - xmltv_id: 'RTBSukmaindera.bn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://www.rtb.gov.bn/PublishingImages/SitePages/Programme%20Guide/Sukmaindera%2011%20November%202021.pdf' - ) -}) - -it('can parse Sukmaindera 11 November 2021.pdf', done => { - const buffer = fs.readFileSync( - path.resolve(__dirname, '__data__/Sukmaindera 11 November 2021.pdf'), - { - charset: 'utf8' - } - ) - parser({ buffer, date }) - .then(results => { - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results.length).toBe(47) - expect(results[0]).toMatchObject({ - start: '2021-11-10T22:00:00.000Z', - stop: '2021-11-10T22:05:00.000Z', - title: 'NATIONAL ANTHEM' - }) - expect(results[46]).toMatchObject({ - start: '2021-11-11T21:30:00.000Z', - stop: '2021-11-11T22:30:00.000Z', - title: 'BACAAN SURAH YASSIN' - }) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can parse Aneka 11 November 2021.pdf', done => { - const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/Aneka 11 November 2021.pdf'), { - charset: 'utf8' - }) - parser({ buffer, date }) - .then(results => { - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(results.length).toBe(26) - expect(results[4]).toMatchObject({ - start: '2021-11-11T03:00:00.000Z', - stop: '2021-11-11T04:05:00.000Z', - title: 'DRAMA TURKI:' - }) - done() - }) - .catch(error => { - done(error) - }) -}) - -it('can handle empty guide', done => { - parser({ - date, - channel, - content: `Object moved -

    Object moved to here.

    - -` - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(error => { - done(error) - }) -}) +// npm run grab -- --site=rtb.gov.bn + +const { parser, url } = require('./rtb.gov.bn.config.js') +const path = require('path') +const fs = require('fs') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Sukmaindera', + xmltv_id: 'RTBSukmaindera.bn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://www.rtb.gov.bn/PublishingImages/SitePages/Programme%20Guide/Sukmaindera%2011%20November%202021.pdf' + ) +}) + +it('can parse Sukmaindera 11 November 2021.pdf', done => { + const buffer = fs.readFileSync( + path.resolve(__dirname, '__data__/Sukmaindera 11 November 2021.pdf'), + { + charset: 'utf8' + } + ) + parser({ buffer, date }) + .then(results => { + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results.length).toBe(47) + expect(results[0]).toMatchObject({ + start: '2021-11-10T22:00:00.000Z', + stop: '2021-11-10T22:05:00.000Z', + title: 'NATIONAL ANTHEM' + }) + expect(results[46]).toMatchObject({ + start: '2021-11-11T21:30:00.000Z', + stop: '2021-11-11T22:30:00.000Z', + title: 'BACAAN SURAH YASSIN' + }) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can parse Aneka 11 November 2021.pdf', done => { + const buffer = fs.readFileSync(path.resolve(__dirname, '__data__/Aneka 11 November 2021.pdf'), { + charset: 'utf8' + }) + parser({ buffer, date }) + .then(results => { + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(results.length).toBe(26) + expect(results[4]).toMatchObject({ + start: '2021-11-11T03:00:00.000Z', + stop: '2021-11-11T04:05:00.000Z', + title: 'DRAMA TURKI:' + }) + done() + }) + .catch(error => { + done(error) + }) +}) + +it('can handle empty guide', done => { + parser({ + date, + channel, + content: `Object moved +

    Object moved to here.

    + +` + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(error => { + done(error) + }) +}) diff --git a/sites/rthk.hk/rthk.hk.config.js b/sites/rthk.hk/rthk.hk.config.js index 76f9987e..cf500571 100644 --- a/sites/rthk.hk/rthk.hk.config.js +++ b/sites/rthk.hk/rthk.hk.config.js @@ -1,89 +1,89 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rthk.hk', - days: 2, - request: { - headers({ channel }) { - return { - Cookie: `lang=${channel.lang}` - } - }, - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ date }) { - return `https://www.rthk.hk/timetable/main_timetable/${date.format('YYYYMMDD')}` - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - for (let item of items) { - const $item = cheerio.load(item) - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - categories: parseCategories($item), - icon: parseIcon($item), - start: parseStart($item, date), - stop: parseStop($item, date) - }) - } - - return programs - } -} - -function parseIcon($item) { - return $item('.single-wrap').data('p') -} - -function parseCategories($item) { - let cate = $item('.single-wrap').data('cate') || '' - let [, categories] = cate.match(/^\|(.*)\|$/) || [null, ''] - - return categories.split('||').filter(Boolean) -} - -function parseTitle($item) { - return $item('.showTit').attr('title') -} - -function parseSubTitle($item) { - return $item('.showEpi').attr('title') -} - -function parseStart($item, date) { - const timeRow = $item('.timeRow').text().trim() - const [, HH, mm] = timeRow.match(/^(\d+):(\d+)-/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') -} - -function parseStop($item, date) { - const timeRow = $item('.timeRow').text().trim() - const [, HH, mm] = timeRow.match(/-(\d+):(\d+)$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.result)) return [] - const channelData = data.result.find(i => i.key == channel.site_id) - if (!channelData || !channelData.data) return [] - const $ = cheerio.load(channelData.data) - - return $('.showWrap').toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rthk.hk', + days: 2, + request: { + headers({ channel }) { + return { + Cookie: `lang=${channel.lang}` + } + }, + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ date }) { + return `https://www.rthk.hk/timetable/main_timetable/${date.format('YYYYMMDD')}` + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + for (let item of items) { + const $item = cheerio.load(item) + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + categories: parseCategories($item), + icon: parseIcon($item), + start: parseStart($item, date), + stop: parseStop($item, date) + }) + } + + return programs + } +} + +function parseIcon($item) { + return $item('.single-wrap').data('p') +} + +function parseCategories($item) { + let cate = $item('.single-wrap').data('cate') || '' + let [, categories] = cate.match(/^\|(.*)\|$/) || [null, ''] + + return categories.split('||').filter(Boolean) +} + +function parseTitle($item) { + return $item('.showTit').attr('title') +} + +function parseSubTitle($item) { + return $item('.showEpi').attr('title') +} + +function parseStart($item, date) { + const timeRow = $item('.timeRow').text().trim() + const [, HH, mm] = timeRow.match(/^(\d+):(\d+)-/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') +} + +function parseStop($item, date) { + const timeRow = $item('.timeRow').text().trim() + const [, HH, mm] = timeRow.match(/-(\d+):(\d+)$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Asia/Hong_Kong') +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.result)) return [] + const channelData = data.result.find(i => i.key == channel.site_id) + if (!channelData || !channelData.data) return [] + const $ = cheerio.load(channelData.data) + + return $('.showWrap').toArray() +} diff --git a/sites/rthk.hk/rthk.hk.test.js b/sites/rthk.hk/rthk.hk.test.js index 2917f1eb..b11edc36 100644 --- a/sites/rthk.hk/rthk.hk.test.js +++ b/sites/rthk.hk/rthk.hk.test.js @@ -1,82 +1,82 @@ -// npm run grab -- --site=rthk.hk - -const { parser, url, request } = require('./rthk.hk.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '31', - xmltv_id: 'RTHKTV31.hk', - lang: 'zh' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.rthk.hk/timetable/main_timetable/20221202') -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - Cookie: 'lang=zh' - }) -}) - -it('can generate valid request headers for English version', () => { - const channelEN = { ...channel, lang: 'en' } - - expect(request.headers({ channel: channelEN })).toMatchObject({ - Cookie: 'lang=en' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zh.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T16:00:00.000Z', - stop: '2022-12-01T17:00:00.000Z', - title: '問天', - sub_title: '第十四集', - categories: ['戲劇'], - icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' - }) -}) - -it('can parse response in English', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-12-01T16:00:00.000Z', - stop: '2022-12-01T17:00:00.000Z', - title: 'The Great Aerospace', - sub_title: 'Episode 14', - categories: ['戲劇'], - icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ date, channel, content }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=rthk.hk + +const { parser, url, request } = require('./rthk.hk.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '31', + xmltv_id: 'RTHKTV31.hk', + lang: 'zh' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.rthk.hk/timetable/main_timetable/20221202') +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + Cookie: 'lang=zh' + }) +}) + +it('can generate valid request headers for English version', () => { + const channelEN = { ...channel, lang: 'en' } + + expect(request.headers({ channel: channelEN })).toMatchObject({ + Cookie: 'lang=en' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_zh.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T16:00:00.000Z', + stop: '2022-12-01T17:00:00.000Z', + title: '問天', + sub_title: '第十四集', + categories: ['戲劇'], + icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' + }) +}) + +it('can parse response in English', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-12-01T16:00:00.000Z', + stop: '2022-12-01T17:00:00.000Z', + title: 'The Great Aerospace', + sub_title: 'Episode 14', + categories: ['戲劇'], + icon: 'https://www.rthk.hk/assets/images/rthk/dtt31/thegreataerospace/10239_1920_s.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ date, channel, content }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js index d3cc83b6..2f68089d 100644 --- a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js +++ b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.config.js @@ -1,45 +1,45 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'rtmklik.rtm.gov.my', - days: 2, - url: function ({ date, channel }) { - return `https://rtm.glueapi.io/v3/epg/${ - channel.site_id - }/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format( - 'YYYY-MM-DD' - )}&timezone=0` - // return `https://rtm.glueapi.io/v3/epg/99/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format('YYYY-MM-DD')}&timezone=0` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.programTitle, - description: item.description, - start: parseTime(item.dateTimeStart), - stop: parseTime(item.dateTimeEnd) - }) - }) - - return programs - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data.schedule ? data.schedule : [] -} - -function parseTime(time) { - return dayjs.utc(time, 'YYYY-MM-DDTHH:mm:ss') -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'rtmklik.rtm.gov.my', + days: 2, + url: function ({ date, channel }) { + return `https://rtm.glueapi.io/v3/epg/${ + channel.site_id + }/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format( + 'YYYY-MM-DD' + )}&timezone=0` + // return `https://rtm.glueapi.io/v3/epg/99/ChannelSchedule?dateStart=${date.format('YYYY-MM-DD')}&dateEnd=${date.format('YYYY-MM-DD')}&timezone=0` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.programTitle, + description: item.description, + start: parseTime(item.dateTimeStart), + stop: parseTime(item.dateTimeEnd) + }) + }) + + return programs + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data.schedule ? data.schedule : [] +} + +function parseTime(time) { + return dayjs.utc(time, 'YYYY-MM-DDTHH:mm:ss') +} diff --git a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js index 23c8a181..23f7b280 100644 --- a/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js +++ b/sites/rtmklik.rtm.gov.my/rtmklik.rtm.gov.my.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=rtmklik.rtm.gov.my - -const { parser, url } = require('./rtmklik.rtm.gov.my.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-09-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'TV2.my' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0' - ) -}) - -it('can parse response', () => { - const content = - '{"id":2,"channel":"TV2","channelId":"2","image":"/live_channel/tv2_Trans.png","idAuthor":9,"type":"TV","timezone":8,"dateCreated":"2022-07-08T01:22:33.233","dateModified":"2022-07-21T21:58:39.77","itemCount":30,"prev":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-03&dateEnd=2022-09-03","next":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-05&dateEnd=2022-09-05","schedule":[{"idEPGProgramSchedule":109303,"dateTimeStart":"2022-09-04T19:00:00","dateTimeEnd":"2022-09-04T20:00:00","scheduleProgramTitle":"Hope Of Life","scheduleProgramDescription":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","scheduleEpisodeNumber":0,"scheduleSeries":0,"duration":3600,"idEPGProgram":3603,"programTitle":"Hope Of Life","description":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","episodeNumber":0,"series":0,"repeat":"Never","dateModified":"2022-08-29T02:14:56.647","dateCreated":"0001-01-01T00:00:00"}]}' - - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-09-04T19:00:00.000Z', - stop: '2022-09-04T20:00:00.000Z', - title: 'Hope Of Life', - description: - 'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"id":2,"channel":"TV2","channelId":"2","schedule":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=rtmklik.rtm.gov.my + +const { parser, url } = require('./rtmklik.rtm.gov.my.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-09-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'TV2.my' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://rtm.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-04&dateEnd=2022-09-04&timezone=0' + ) +}) + +it('can parse response', () => { + const content = + '{"id":2,"channel":"TV2","channelId":"2","image":"/live_channel/tv2_Trans.png","idAuthor":9,"type":"TV","timezone":8,"dateCreated":"2022-07-08T01:22:33.233","dateModified":"2022-07-21T21:58:39.77","itemCount":30,"prev":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-03&dateEnd=2022-09-03","next":"https://rtm-admin.glueapi.io/v3/epg/2/ChannelSchedule?dateStart=2022-09-05&dateEnd=2022-09-05","schedule":[{"idEPGProgramSchedule":109303,"dateTimeStart":"2022-09-04T19:00:00","dateTimeEnd":"2022-09-04T20:00:00","scheduleProgramTitle":"Hope Of Life","scheduleProgramDescription":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","scheduleEpisodeNumber":0,"scheduleSeries":0,"duration":3600,"idEPGProgram":3603,"programTitle":"Hope Of Life","description":"Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.","episodeNumber":0,"series":0,"repeat":"Never","dateModified":"2022-08-29T02:14:56.647","dateCreated":"0001-01-01T00:00:00"}]}' + + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-09-04T19:00:00.000Z', + stop: '2022-09-04T20:00:00.000Z', + title: 'Hope Of Life', + description: + 'Kisah kehidupan 3 pakar bedah yang berbeza status dan latar belakang, namun mereka komited dengan kerjaya mereka sebagai doktor. Lakonan : Johnson Low, Elvis Chin, Mayjune Tan, Kelvin Liew, Jacky Kam dan Katrina Ho.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"id":2,"channel":"TV2","channelId":"2","schedule":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/rtp.pt/rtp.pt.config.js b/sites/rtp.pt/rtp.pt.config.js index 12e552cf..a8d00441 100644 --- a/sites/rtp.pt/rtp.pt.config.js +++ b/sites/rtp.pt/rtp.pt.config.js @@ -1,81 +1,81 @@ -const _ = require('lodash') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const tz = { - lis: 'Europe/Lisbon', - per: 'Asia/Macau', - rja: 'America/Sao_Paulo' -} - -module.exports = { - site: 'rtp.pt', - days: 2, - url({ channel, date }) { - let [region, channelCode] = channel.site_id.split('#') - return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format( - 'D-M-YYYY' - )}/${region}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, channel) - if (!start) return - if (prev) { - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: item.name, - description: item.description, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://www.rtp.pt/EPG/json/rtp-home-page/list-channels/tv') - .then(r => r.data.result) - .catch(console.error) - - return items.map(i => { - return { - lang: 'pt', - site_id: i.channel_code, - name: i.channel_name - } - }) - } -} - -function parseIcon(item) { - const last = item.image.pop() - if (!last) return null - return last.src -} - -function parseStart(item, channel) { - let [region] = channel.site_id.split('#') - return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region]) -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - - return _.flatten(Object.values(data.result)) -} +const _ = require('lodash') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const tz = { + lis: 'Europe/Lisbon', + per: 'Asia/Macau', + rja: 'America/Sao_Paulo' +} + +module.exports = { + site: 'rtp.pt', + days: 2, + url({ channel, date }) { + let [region, channelCode] = channel.site_id.split('#') + return `https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/${channelCode}/${date.format( + 'D-M-YYYY' + )}/${region}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, channel) + if (!start) return + if (prev) { + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: item.name, + description: item.description, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://www.rtp.pt/EPG/json/rtp-home-page/list-channels/tv') + .then(r => r.data.result) + .catch(console.error) + + return items.map(i => { + return { + lang: 'pt', + site_id: i.channel_code, + name: i.channel_name + } + }) + } +} + +function parseIcon(item) { + const last = item.image.pop() + if (!last) return null + return last.src +} + +function parseStart(item, channel) { + let [region] = channel.site_id.split('#') + return dayjs.tz(item.date, 'YYYY-MM-DD HH:mm:ss', tz[region]) +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + + return _.flatten(Object.values(data.result)) +} diff --git a/sites/rtp.pt/rtp.pt.test.js b/sites/rtp.pt/rtp.pt.test.js index 6b1f9826..fd25ba3c 100644 --- a/sites/rtp.pt/rtp.pt.test.js +++ b/sites/rtp.pt/rtp.pt.test.js @@ -1,45 +1,45 @@ -// npm run channels:parse -- --config=./sites/rtp.pt/rtp.pt.config.js --output=./sites/rtp.pt/rtp.pt.channels.xml -// npm run grab -- --site=rtp.pt - -const { parser, url } = require('./rtp.pt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lis#4', - xmltv_id: 'RTPMadeira.pt' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/4/2-12-2022/lis' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[9]).toMatchObject({ - start: '2022-12-02T23:30:00.000Z', - stop: '2022-12-03T00:00:00.000Z', - title: 'Telejornal Madeira', - description: 'Informação de proximidade. De confiança!', - icon: 'https://cdn-images.rtp.pt/EPG/imagens/15790_43438_8820.png?w=384&h=216' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', channel, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/rtp.pt/rtp.pt.config.js --output=./sites/rtp.pt/rtp.pt.channels.xml +// npm run grab -- --site=rtp.pt + +const { parser, url } = require('./rtp.pt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-12-02', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lis#4', + xmltv_id: 'RTPMadeira.pt' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.rtp.pt/EPG/json/rtp-channels-page/list-grid/tv/4/2-12-2022/lis' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[9]).toMatchObject({ + start: '2022-12-02T23:30:00.000Z', + stop: '2022-12-03T00:00:00.000Z', + title: 'Telejornal Madeira', + description: 'Informação de proximidade. De confiança!', + icon: 'https://cdn-images.rtp.pt/EPG/imagens/15790_43438_8820.png?w=384&h=216' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/ruv.is/ruv.is.config.js b/sites/ruv.is/ruv.is.config.js index 355fd9dd..9406ab26 100644 --- a/sites/ruv.is/ruv.is.config.js +++ b/sites/ruv.is/ruv.is.config.js @@ -1,79 +1,79 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'ruv.is', - days: 2, - url({ channel, date }) { - let params = new URLSearchParams() - params.append('operationName', 'getSchedule') - params.append( - 'variables', - JSON.stringify({ channel: channel.site_id, date: date.format('YYYY-MM-DD') }) - ) - params.append( - 'extensions', - JSON.stringify({ - persistedQuery: { - version: 1, - sha256Hash: '7d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6' - } - }) - ) - - return `https://www.ruv.is/gql/?${params.toString()}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - let start = parseStart(item, date) - let stop = parseStop(item, date) - if (stop.isBefore(start)) { - stop = stop.add(1, 'd') - } - programs.push({ - title: item.title, - description: item.description, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.image.replace('$$IMAGESIZE$$', '480') -} - -function parseStart(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.start_time_friendly}`, - 'YYYY-MM-DD HH:mm', - 'Atlantic/Reykjavik' - ) -} - -function parseStop(item, date) { - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${item.end_time_friendly}`, - 'YYYY-MM-DD HH:mm', - 'Atlantic/Reykjavik' - ) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data.Schedule.events)) return [] - - return data.data.Schedule.events -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'ruv.is', + days: 2, + url({ channel, date }) { + let params = new URLSearchParams() + params.append('operationName', 'getSchedule') + params.append( + 'variables', + JSON.stringify({ channel: channel.site_id, date: date.format('YYYY-MM-DD') }) + ) + params.append( + 'extensions', + JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: '7d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6' + } + }) + ) + + return `https://www.ruv.is/gql/?${params.toString()}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + let start = parseStart(item, date) + let stop = parseStop(item, date) + if (stop.isBefore(start)) { + stop = stop.add(1, 'd') + } + programs.push({ + title: item.title, + description: item.description, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.image.replace('$$IMAGESIZE$$', '480') +} + +function parseStart(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.start_time_friendly}`, + 'YYYY-MM-DD HH:mm', + 'Atlantic/Reykjavik' + ) +} + +function parseStop(item, date) { + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${item.end_time_friendly}`, + 'YYYY-MM-DD HH:mm', + 'Atlantic/Reykjavik' + ) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data.Schedule.events)) return [] + + return data.data.Schedule.events +} diff --git a/sites/ruv.is/ruv.is.test.js b/sites/ruv.is/ruv.is.test.js index f034d948..a415b30a 100644 --- a/sites/ruv.is/ruv.is.test.js +++ b/sites/ruv.is/ruv.is.test.js @@ -1,48 +1,48 @@ -// npm run grab -- --site=ruv.is - -const { parser, url } = require('./ruv.is.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'ruv', - xmltv_id: 'RUV.is' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.ruv.is/gql/?operationName=getSchedule&variables=%7B%22channel%22%3A%22ruv%22%2C%22date%22%3A%222023-01-17%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%227d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6%22%7D%7D' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-17T13:00:00.000Z', - stop: '2023-01-17T13:10:00.000Z', - title: 'Heimaleikfimi', - description: - 'Góð ráð og æfingar sem tilvalið er að gera heima. Íris Rut Garðarsdóttir sjúkraþjálfari hefur umsjón með leikfiminni. e.', - icon: 'https://d38kdhuogyllre.cloudfront.net/fit-in/480x/filters:quality(65)/hd_posters/91pvig-3p3hig.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=ruv.is + +const { parser, url } = require('./ruv.is.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'ruv', + xmltv_id: 'RUV.is' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.ruv.is/gql/?operationName=getSchedule&variables=%7B%22channel%22%3A%22ruv%22%2C%22date%22%3A%222023-01-17%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%227d133b9bd9e50127e90f2b3af1b41eb5e89cd386ed9b100b55169f395af350e6%22%7D%7D' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-17T13:00:00.000Z', + stop: '2023-01-17T13:10:00.000Z', + title: 'Heimaleikfimi', + description: + 'Góð ráð og æfingar sem tilvalið er að gera heima. Íris Rut Garðarsdóttir sjúkraþjálfari hefur umsjón með leikfiminni. e.', + icon: 'https://d38kdhuogyllre.cloudfront.net/fit-in/480x/filters:quality(65)/hd_posters/91pvig-3p3hig.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sat.tv/sat.tv.config.js b/sites/sat.tv/sat.tv.config.js index e3dc429d..0fc8e652 100644 --- a/sites/sat.tv/sat.tv.config.js +++ b/sites/sat.tv/sat.tv.config.js @@ -1,129 +1,129 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php' - -module.exports = { - site: 'sat.tv', - days: 2, - url: API_ENDPOINT, - request: { - method: 'POST', - headers({ channel }) { - return { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: `pll_language=${channel.lang}` - } - }, - data({ channel, date }) { - const [satSatellite, satLineup] = channel.site_id.split('#') - const params = new URLSearchParams() - params.append('dateFiltre', date.format('YYYY-MM-DD')) - params.append('hoursFiltre', '0') - params.append('satLineup', satLineup) - params.append('satSatellite', satSatellite) - params.append('userDateTime', date.valueOf()) - params.append('userTimezone', 'Europe/London') - - return params - } - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - let $item = cheerio.load(item) - let start = parseStart($item, date) - let duration = parseDuration($item) - let stop = start.add(duration, 'm') - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels({ lang, satLineup, satSatellite }) { - const params = new URLSearchParams() - params.append('dateFiltre', dayjs().format('YYYY-MM-DD')) - params.append('hoursFiltre', '0') - params.append('satLineup', satLineup) - params.append('satSatellite', satSatellite) - params.append('userDateTime', dayjs().valueOf()) - params.append('userTimezone', 'Europe/London') - const data = await axios - .post(API_ENDPOINT, params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: `pll_language=${lang}` - } - }) - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const channels = $('.main-container-channels-events > .container-channel-events').toArray() - - return channels.map(item => { - const name = $(item).find('.channel-title').text().trim() - return { - lang, - site_id: name, - name - } - }) - } -} - -function parseIcon($item) { - const src = $item('.event-logo img:not(.no-img)').attr('src') - - return src ? `https://sat.tv${src}` : null -} - -function parseTitle($item) { - return $item('.event-data-title').text() -} - -function parseDescription($item) { - return $item('.event-data-desc').text() -} - -function parseStart($item, date) { - let eventDataDate = $item('.event-data-date').text().trim() - let [, time] = eventDataDate.match(/(\d{2}:\d{2})/) || [null, null] - if (!time) return null - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseDuration($item) { - let eventDataInfo = $item('.event-data-info').text().trim() - let [, h, m] = eventDataInfo.match(/(\d{2})h(\d{2})/) || [null, 0, 0] - - return parseInt(h) * 60 + parseInt(m) -} - -function parseItems(content, channel) { - const [, , site_id] = channel.site_id.split('#') - const $ = cheerio.load(content) - const channelData = $('.main-container-channels-events > .container-channel-events') - .filter((index, el) => { - return $(el).find('.channel-title').text().trim() === site_id - }) - .first() - if (!channelData) return [] - - return $(channelData).find('.container-event').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php' + +module.exports = { + site: 'sat.tv', + days: 2, + url: API_ENDPOINT, + request: { + method: 'POST', + headers({ channel }) { + return { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: `pll_language=${channel.lang}` + } + }, + data({ channel, date }) { + const [satSatellite, satLineup] = channel.site_id.split('#') + const params = new URLSearchParams() + params.append('dateFiltre', date.format('YYYY-MM-DD')) + params.append('hoursFiltre', '0') + params.append('satLineup', satLineup) + params.append('satSatellite', satSatellite) + params.append('userDateTime', date.valueOf()) + params.append('userTimezone', 'Europe/London') + + return params + } + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + let $item = cheerio.load(item) + let start = parseStart($item, date) + let duration = parseDuration($item) + let stop = start.add(duration, 'm') + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels({ lang, satLineup, satSatellite }) { + const params = new URLSearchParams() + params.append('dateFiltre', dayjs().format('YYYY-MM-DD')) + params.append('hoursFiltre', '0') + params.append('satLineup', satLineup) + params.append('satSatellite', satSatellite) + params.append('userDateTime', dayjs().valueOf()) + params.append('userTimezone', 'Europe/London') + const data = await axios + .post(API_ENDPOINT, params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: `pll_language=${lang}` + } + }) + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const channels = $('.main-container-channels-events > .container-channel-events').toArray() + + return channels.map(item => { + const name = $(item).find('.channel-title').text().trim() + return { + lang, + site_id: name, + name + } + }) + } +} + +function parseIcon($item) { + const src = $item('.event-logo img:not(.no-img)').attr('src') + + return src ? `https://sat.tv${src}` : null +} + +function parseTitle($item) { + return $item('.event-data-title').text() +} + +function parseDescription($item) { + return $item('.event-data-desc').text() +} + +function parseStart($item, date) { + let eventDataDate = $item('.event-data-date').text().trim() + let [, time] = eventDataDate.match(/(\d{2}:\d{2})/) || [null, null] + if (!time) return null + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseDuration($item) { + let eventDataInfo = $item('.event-data-info').text().trim() + let [, h, m] = eventDataInfo.match(/(\d{2})h(\d{2})/) || [null, 0, 0] + + return parseInt(h) * 60 + parseInt(m) +} + +function parseItems(content, channel) { + const [, , site_id] = channel.site_id.split('#') + const $ = cheerio.load(content) + const channelData = $('.main-container-channels-events > .container-channel-events') + .filter((index, el) => { + return $(el).find('.channel-title').text().trim() === site_id + }) + .first() + if (!channelData) return [] + + return $(channelData).find('.container-event').toArray() +} diff --git a/sites/sat.tv/sat.tv.test.js b/sites/sat.tv/sat.tv.test.js index ee30ef07..2a7769b0 100644 --- a/sites/sat.tv/sat.tv.test.js +++ b/sites/sat.tv/sat.tv.test.js @@ -1,112 +1,112 @@ -// npm run channels:parse -- --config=sites/sat.tv/sat.tv.config.js --output=sites/sat.tv/sat.tv.channels.xml --set=lang:ar --set=satSatellite:1 --set=satLineup:38 -// npm run grab -- --site=sat.tv - -const { parser, url, request } = require('./sat.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1#38#السعودية', - xmltv_id: 'AlSaudiya.sa', - lang: 'ar' -} - -it('can generate valid url', () => { - expect(url).toBe('https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers({ channel })).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - Cookie: 'pll_language=ar' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ channel, date }) - expect(data.get('dateFiltre')).toBe('2023-06-26') - expect(data.get('hoursFiltre')).toBe('0') - expect(data.get('satLineup')).toBe('38') - expect(data.get('satSatellite')).toBe('1') - expect(data.get('userDateTime')).toBe('1687737600000') - expect(data.get('userTimezone')).toBe('Europe/London') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ar.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(35) - expect(results[0]).toMatchObject({ - start: '2023-06-26T06:30:00.000Z', - stop: '2023-06-26T07:00:00.000Z', - title: 'تعظيم البلد الحرام', - description: `الناس, دين, ثقافة -يلقي صانع الفيلم الضوء على مشروع تعظيم البلد الحرام في مكة من العائلة الملكية في المملكة العربية السعودية، والذي يهدف لإبراز حرمته لدى المسلمين حول العالم.`, - icon: null - }) - - expect(results[34]).toMatchObject({ - start: '2023-06-26T22:30:00.000Z', - stop: '2023-06-27T01:00:00.000Z', - title: 'الأخبار', - description: `نشرة -.يطرح أهم القضايا والأحداث على الساحة السعودية والعالمية`, - icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' - }) -}) - -it('can parse response in english', () => { - const channel = { - site_id: '1#38#Saudi HD', - xmltv_id: 'AlSaudiya.sa', - lang: 'en' - } - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.html')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(32) - expect(results[0]).toMatchObject({ - start: '2023-06-26T09:00:00.000Z', - stop: '2023-06-26T10:00:00.000Z', - title: 'News', - description: `Newscast -The most important issues and events on the Saudi and the world.`, - icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' - }) - - expect(results[31]).toMatchObject({ - start: '2023-06-26T23:15:00.000Z', - stop: '2023-06-27T00:00:00.000Z', - title: "Bride's Father", - description: `Romance, Drama, Family -2022 -Abdelhamid's family struggles to deal with the challenges of life that keep flowing one by one. they manage to stay strong-armed with their love and trust for each other. -Sayed Ragab, Sawsan Badr, Medhat Saleh, Nermine Al Feqy, Mohamed Adel, Khaled Kamal, Rania Farid, Hani Kamal, Hani Kamal`, - icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3157177.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, date, channel }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/sat.tv/sat.tv.config.js --output=sites/sat.tv/sat.tv.channels.xml --set=lang:ar --set=satSatellite:1 --set=satLineup:38 +// npm run grab -- --site=sat.tv + +const { parser, url, request } = require('./sat.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1#38#السعودية', + xmltv_id: 'AlSaudiya.sa', + lang: 'ar' +} + +it('can generate valid url', () => { + expect(url).toBe('https://sat.tv/wp-content/themes/twentytwenty-child/ajax_chaines.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers({ channel })).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: 'pll_language=ar' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ channel, date }) + expect(data.get('dateFiltre')).toBe('2023-06-26') + expect(data.get('hoursFiltre')).toBe('0') + expect(data.get('satLineup')).toBe('38') + expect(data.get('satSatellite')).toBe('1') + expect(data.get('userDateTime')).toBe('1687737600000') + expect(data.get('userTimezone')).toBe('Europe/London') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_ar.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(35) + expect(results[0]).toMatchObject({ + start: '2023-06-26T06:30:00.000Z', + stop: '2023-06-26T07:00:00.000Z', + title: 'تعظيم البلد الحرام', + description: `الناس, دين, ثقافة +يلقي صانع الفيلم الضوء على مشروع تعظيم البلد الحرام في مكة من العائلة الملكية في المملكة العربية السعودية، والذي يهدف لإبراز حرمته لدى المسلمين حول العالم.`, + icon: null + }) + + expect(results[34]).toMatchObject({ + start: '2023-06-26T22:30:00.000Z', + stop: '2023-06-27T01:00:00.000Z', + title: 'الأخبار', + description: `نشرة +.يطرح أهم القضايا والأحداث على الساحة السعودية والعالمية`, + icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' + }) +}) + +it('can parse response in english', () => { + const channel = { + site_id: '1#38#Saudi HD', + xmltv_id: 'AlSaudiya.sa', + lang: 'en' + } + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_en.html')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(32) + expect(results[0]).toMatchObject({ + start: '2023-06-26T09:00:00.000Z', + stop: '2023-06-26T10:00:00.000Z', + title: 'News', + description: `Newscast +The most important issues and events on the Saudi and the world.`, + icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3077892.jpg' + }) + + expect(results[31]).toMatchObject({ + start: '2023-06-26T23:15:00.000Z', + stop: '2023-06-27T00:00:00.000Z', + title: "Bride's Father", + description: `Romance, Drama, Family +2022 +Abdelhamid's family struggles to deal with the challenges of life that keep flowing one by one. they manage to stay strong-armed with their love and trust for each other. +Sayed Ragab, Sawsan Badr, Medhat Saleh, Nermine Al Feqy, Mohamed Adel, Khaled Kamal, Rania Farid, Hani Kamal, Hani Kamal`, + icon: 'https://sat.tv/wp-content/themes/twentytwenty-child/data_lineups/nilesat/images3/epg-3157177.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, date, channel }) + expect(result).toMatchObject([]) +}) diff --git a/sites/siba.com.co/siba.com.co.config.js b/sites/siba.com.co/siba.com.co.config.js index d1ef37d2..970815be 100644 --- a/sites/siba.com.co/siba.com.co.config.js +++ b/sites/siba.com.co/siba.com.co.config.js @@ -1,56 +1,56 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'siba.com.co', - days: 2, - url: 'http://devportal.siba.com.co/index.php?action=grilla', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('servicio', '10') - params.append('ini', date.unix()) - params.append('end', date.add(1, 'd').unix()) - params.append('chn', channel.site_id) - - return params - } - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.nom, - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.unix(item.ini) -} - -function parseStop(item) { - return dayjs.unix(item.fin) -} - -function parseContent(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.list)) return null - - return data.list.find(i => i.id === channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - - return data ? data.prog : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'siba.com.co', + days: 2, + url: 'http://devportal.siba.com.co/index.php?action=grilla', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('servicio', '10') + params.append('ini', date.unix()) + params.append('end', date.add(1, 'd').unix()) + params.append('chn', channel.site_id) + + return params + } + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.nom, + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.unix(item.ini) +} + +function parseStop(item) { + return dayjs.unix(item.fin) +} + +function parseContent(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.list)) return null + + return data.list.find(i => i.id === channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + + return data ? data.prog : [] +} diff --git a/sites/siba.com.co/siba.com.co.test.js b/sites/siba.com.co/siba.com.co.test.js index a466ded6..c5f7bc26 100644 --- a/sites/siba.com.co/siba.com.co.test.js +++ b/sites/siba.com.co/siba.com.co.test.js @@ -1,54 +1,54 @@ -// npm run grab -- --site=siba.com.co - -const { parser, url, request } = require('./siba.com.co.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '395', - xmltv_id: 'CanalClaro.cl' -} -const content = - '{"list":[{"id":"395","nom":"CANAL CLARO","num":"102","logo":"7c4b9e8566a6e867d1db4c7ce845f1f4.jpg","cat":"Exclusivos Claro","prog":[{"id":"665724465","nom":"Worst Cooks In America","ini":1636588800,"fin":1636592400}]}],"error":null}' - -it('can generate valid url', () => { - expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.has('servicio')).toBe(true) - expect(result.has('ini')).toBe(true) - expect(result.has('end')).toBe(true) - expect(result.has('chn')).toBe(true) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-11T00:00:00.000Z', - stop: '2021-11-11T01:00:00.000Z', - title: 'Worst Cooks In America' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"list":[],"error":null}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=siba.com.co + +const { parser, url, request } = require('./siba.com.co.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '395', + xmltv_id: 'CanalClaro.cl' +} +const content = + '{"list":[{"id":"395","nom":"CANAL CLARO","num":"102","logo":"7c4b9e8566a6e867d1db4c7ce845f1f4.jpg","cat":"Exclusivos Claro","prog":[{"id":"665724465","nom":"Worst Cooks In America","ini":1636588800,"fin":1636592400}]}],"error":null}' + +it('can generate valid url', () => { + expect(url).toBe('http://devportal.siba.com.co/index.php?action=grilla') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.has('servicio')).toBe(true) + expect(result.has('ini')).toBe(true) + expect(result.has('end')).toBe(true) + expect(result.has('chn')).toBe(true) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-11T00:00:00.000Z', + stop: '2021-11-11T01:00:00.000Z', + title: 'Worst Cooks In America' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"list":[],"error":null}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/singtel.com/singtel.com.config.js b/sites/singtel.com/singtel.com.config.js index f15d1b48..51215bab 100644 --- a/sites/singtel.com/singtel.com.config.js +++ b/sites/singtel.com/singtel.com.config.js @@ -1,47 +1,47 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'singtel.com', - days: 3, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - return `https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/${date.format( - 'DDMMYYYY' - )}.json` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = dayjs.tz(item.startDateTime, 'Asia/Singapore') - const stop = start.add(item.duration, 's') - programs.push({ - title: item.program.title, - category: item.program.subCategory, - description: item.program.description, - start, - stop - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - return data && data[channel.site_id] ? data[channel.site_id] : [] - } catch (err) { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'singtel.com', + days: 3, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + return `https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/${date.format( + 'DDMMYYYY' + )}.json` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = dayjs.tz(item.startDateTime, 'Asia/Singapore') + const stop = start.add(item.duration, 's') + programs.push({ + title: item.program.title, + category: item.program.subCategory, + description: item.program.description, + start, + stop + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + return data && data[channel.site_id] ? data[channel.site_id] : [] + } catch (err) { + return [] + } +} diff --git a/sites/singtel.com/singtel.com.test.js b/sites/singtel.com/singtel.com.test.js index 9daa2fa8..f20cf57e 100644 --- a/sites/singtel.com/singtel.com.test.js +++ b/sites/singtel.com/singtel.com.test.js @@ -1,62 +1,62 @@ -// npm run channels:parse -- --config=./sites/singtel.com/singtel.com.config.js --output=./sites/singtel.com/singtel.com.channels.xml -// npm run grab -- --site=singtel.com - -const { parser, url } = require('./singtel.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '5418', - xmltv_id: 'ParamountNetworkSingapore.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/29012023.json' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(23) - expect(results[0]).toMatchObject({ - start: '2023-01-28T16:00:00.000Z', - stop: '2023-01-28T17:30:00.000Z', - title: 'Hip Hop Family Christmas Wedding', - description: - 'Hip Hop\'s most famous family is back, and this time Christmas wedding bells are ringing! Jessica and Jayson are getting ready to say their "I do\'s".', - category: 'Specials' - }) - - expect(results[10]).toMatchObject({ - start: '2023-01-29T01:00:00.000Z', - stop: '2023-01-29T01:30:00.000Z', - title: 'The Daily Show', - description: - 'The Daily Show correspondents tackle the biggest stories in news, politics and pop culture.', - category: 'English Entertainment' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const results = parser({ content, channel }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/singtel.com/singtel.com.config.js --output=./sites/singtel.com/singtel.com.channels.xml +// npm run grab -- --site=singtel.com + +const { parser, url } = require('./singtel.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '5418', + xmltv_id: 'ParamountNetworkSingapore.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.singtel.com/etc/singtel/public/tv/epg-parsed-data/29012023.json' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(23) + expect(results[0]).toMatchObject({ + start: '2023-01-28T16:00:00.000Z', + stop: '2023-01-28T17:30:00.000Z', + title: 'Hip Hop Family Christmas Wedding', + description: + 'Hip Hop\'s most famous family is back, and this time Christmas wedding bells are ringing! Jessica and Jayson are getting ready to say their "I do\'s".', + category: 'Specials' + }) + + expect(results[10]).toMatchObject({ + start: '2023-01-29T01:00:00.000Z', + stop: '2023-01-29T01:30:00.000Z', + title: 'The Daily Show', + description: + 'The Daily Show correspondents tackle the biggest stories in news, politics and pop culture.', + category: 'English Entertainment' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const results = parser({ content, channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/sjonvarp.is/sjonvarp.is.config.js b/sites/sjonvarp.is/sjonvarp.is.config.js index d9b15946..6fac4eb4 100644 --- a/sites/sjonvarp.is/sjonvarp.is.config.js +++ b/sites/sjonvarp.is/sjonvarp.is.config.js @@ -1,64 +1,64 @@ -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'sjonvarp.is', - days: 2, - url: function ({ channel, date }) { - return `http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=${channel.site_id}&y=${date.format( - 'YYYY' - )}&m=${date.format('MM')}&d=${date.format('DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.day-listing-title').text() -} - -function parseDescription($item) { - return $item('.day-listing-description').text() -} - -function parseStart($item, date) { - const time = $item('.day-listing-time') - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - 'body > div.container.nano-container > div > ul > div.day-listing > div:not(.day-listing-channel)' - ).toArray() -} +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'sjonvarp.is', + days: 2, + url: function ({ channel, date }) { + return `http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=${channel.site_id}&y=${date.format( + 'YYYY' + )}&m=${date.format('MM')}&d=${date.format('DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.day-listing-title').text() +} + +function parseDescription($item) { + return $item('.day-listing-description').text() +} + +function parseStart($item, date) { + const time = $item('.day-listing-time') + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + 'body > div.container.nano-container > div > ul > div.day-listing > div:not(.day-listing-channel)' + ).toArray() +} diff --git a/sites/sjonvarp.is/sjonvarp.is.test.js b/sites/sjonvarp.is/sjonvarp.is.test.js index a36f9083..90acbc87 100644 --- a/sites/sjonvarp.is/sjonvarp.is.test.js +++ b/sites/sjonvarp.is/sjonvarp.is.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=sjonvarp.is - -const { parser, url } = require('./sjonvarp.is.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'RUV', - xmltv_id: 'RUV.is' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=RUV&y=2022&m=08&d=28' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-28T07:15:00.000Z', - stop: '2022-08-28T07:16:00.000Z', - title: 'KrakkaRÚV' - }) - - expect(results[1]).toMatchObject({ - start: '2022-08-28T07:16:00.000Z', - stop: '2022-08-28T07:21:00.000Z', - title: 'Tölukubbar', - description: 'Lærið um tölustafina með Tölukubbunum! e.' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sjonvarp.is + +const { parser, url } = require('./sjonvarp.is.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'RUV', + xmltv_id: 'RUV.is' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'http://www.sjonvarp.is/index.php?Tm=%3F&p=idag&c=RUV&y=2022&m=08&d=28' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-28T07:15:00.000Z', + stop: '2022-08-28T07:16:00.000Z', + title: 'KrakkaRÚV' + }) + + expect(results[1]).toMatchObject({ + start: '2022-08-28T07:16:00.000Z', + stop: '2022-08-28T07:21:00.000Z', + title: 'Tölukubbar', + description: 'Lærið um tölustafina með Tölukubbunum! e.' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.co.nz/sky.co.nz.config.js b/sites/sky.co.nz/sky.co.nz.config.js index c095e5f3..117fbd9f 100644 --- a/sites/sky.co.nz/sky.co.nz.config.js +++ b/sites/sky.co.nz/sky.co.nz.config.js @@ -1,55 +1,55 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.co.nz', - days: 2, - url({ date, channel }) { - return `https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=${ - channel.site_id - }&start=${date.valueOf()}&end=${date.add(1, 'day').valueOf()}&limit=20000` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.synopsis, - category: item.genres, - rating: parseRating(item), - start: dayjs(parseInt(item.start)), - stop: dayjs(parseInt(item.end)) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://skywebconfig.msl-prod.skycloud.co.nz/sky/json/channels.prod.json') - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'en', - site_id: parseInt(item.number).toString(), - name: item.name - } - }) - } -} - -function parseItems(content) { - const data = JSON.parse(content) - return data && data.events && Array.isArray(data.events) ? data.events : [] -} - -function parseRating(item) { - if (!item.rating) return null - return { - system: 'OFLC', - value: item.rating - } -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.co.nz', + days: 2, + url({ date, channel }) { + return `https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=${ + channel.site_id + }&start=${date.valueOf()}&end=${date.add(1, 'day').valueOf()}&limit=20000` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.synopsis, + category: item.genres, + rating: parseRating(item), + start: dayjs(parseInt(item.start)), + stop: dayjs(parseInt(item.end)) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://skywebconfig.msl-prod.skycloud.co.nz/sky/json/channels.prod.json') + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'en', + site_id: parseInt(item.number).toString(), + name: item.name + } + }) + } +} + +function parseItems(content) { + const data = JSON.parse(content) + return data && data.events && Array.isArray(data.events) ? data.events : [] +} + +function parseRating(item) { + if (!item.rating) return null + return { + system: 'OFLC', + value: item.rating + } +} diff --git a/sites/sky.co.nz/sky.co.nz.test.js b/sites/sky.co.nz/sky.co.nz.test.js index ea6eb2b9..291a2a61 100644 --- a/sites/sky.co.nz/sky.co.nz.test.js +++ b/sites/sky.co.nz/sky.co.nz.test.js @@ -1,62 +1,62 @@ -// npm run grab -- --site=sky.co.nz - -const { parser, url } = require('./sky.co.nz.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '36', - xmltv_id: 'SkyMoviesFamily.nz' -} -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=36&start=1674259200000&end=1674345600000&limit=20000' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result[0]).toMatchObject({ - title: 'Sing 2', - description: - "Animated: Buster Moon and his friends must persuade the world's most reclusive rock star to help launch their most dazzling extravaganza yet. Voices Of: Matthew McConaughey, Reese Witherspoon (2021)", - category: ['Animated'], - rating: { system: 'OFLC', value: 'PG' }, - start: '2023-01-20T23:41:00.000Z', - stop: '2023-01-21T01:28:00.000Z' - }) - - expect(result[5]).toMatchObject({ - title: 'Harry Potter and the Goblet of Fire', - description: - 'Adventure: Harry is selected to represent Hogwarts at a legendary and dangerous wizardry competition between three schools of magic. Stars: Daniel Radcliffe, Rupert Grint (2005)', - category: ['Action/Adventure'], - rating: { system: 'OFLC', value: 'M-V' }, - start: '2023-01-21T07:42:00.000Z', - stop: '2023-01-21T10:13:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const result = parser( - { - content: `{ - "code": "DATE_FORMAT_ERROR", - "description": "DateFormat error", - "message": "Unparseable date: x" - }` - }, - channel - ) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sky.co.nz + +const { parser, url } = require('./sky.co.nz.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '36', + xmltv_id: 'SkyMoviesFamily.nz' +} +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://web-epg.sky.co.nz/prod/epgs/v1?channelNumber=36&start=1674259200000&end=1674345600000&limit=20000' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result[0]).toMatchObject({ + title: 'Sing 2', + description: + "Animated: Buster Moon and his friends must persuade the world's most reclusive rock star to help launch their most dazzling extravaganza yet. Voices Of: Matthew McConaughey, Reese Witherspoon (2021)", + category: ['Animated'], + rating: { system: 'OFLC', value: 'PG' }, + start: '2023-01-20T23:41:00.000Z', + stop: '2023-01-21T01:28:00.000Z' + }) + + expect(result[5]).toMatchObject({ + title: 'Harry Potter and the Goblet of Fire', + description: + 'Adventure: Harry is selected to represent Hogwarts at a legendary and dangerous wizardry competition between three schools of magic. Stars: Daniel Radcliffe, Rupert Grint (2005)', + category: ['Action/Adventure'], + rating: { system: 'OFLC', value: 'M-V' }, + start: '2023-01-21T07:42:00.000Z', + stop: '2023-01-21T10:13:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const result = parser( + { + content: `{ + "code": "DATE_FORMAT_ERROR", + "description": "DateFormat error", + "message": "Unparseable date: x" + }` + }, + channel + ) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.com/sky.com.config.js b/sites/sky.com/sky.com.config.js index cb9aa084..72612bdf 100644 --- a/sites/sky.com/sky.com.config.js +++ b/sites/sky.com/sky.com.config.js @@ -1,33 +1,33 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.com', - days: 2, - url: function ({ date, channel }) { - return `https://epgservices.sky.com/5.2.2/api/2.0/channel/json/${ - channel.site_id - }/${date.unix()}/86400/4` - }, - parser: function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - - items.forEach(item => { - programs.push({ - title: item.t, - description: item.d, - start: dayjs.unix(item.s), - stop: dayjs.unix(item.s + item.m[1]), - icon: item.img ? `http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/${item.img}` : null - }) - }) - - return programs - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - - return data && data.listings ? data.listings[channel.site_id] : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.com', + days: 2, + url: function ({ date, channel }) { + return `https://epgservices.sky.com/5.2.2/api/2.0/channel/json/${ + channel.site_id + }/${date.unix()}/86400/4` + }, + parser: function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + + items.forEach(item => { + programs.push({ + title: item.t, + description: item.d, + start: dayjs.unix(item.s), + stop: dayjs.unix(item.s + item.m[1]), + icon: item.img ? `http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/${item.img}` : null + }) + }) + + return programs + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + + return data && data.listings ? data.listings[channel.site_id] : [] +} diff --git a/sites/sky.com/sky.com.test.js b/sites/sky.com/sky.com.test.js index 91c45b18..5dc8872b 100644 --- a/sites/sky.com/sky.com.test.js +++ b/sites/sky.com/sky.com.test.js @@ -1,54 +1,54 @@ -// npm run grab -- --site=sky.com - -const { parser, url } = require('./sky.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-12-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2002', - xmltv_id: 'BBCOneLondon.uk' -} -const content = - '{"listings":{"2002":[{"s":1639446600,"t":"Question of Sport","audioDescription":false,"m":[27170,1800,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":53228,"d":"14/36. In this Christmas special, Paddy, Sam and Ugo are joined by Anton Du Beke, Shaun Wallace, Big Zuu and Jules Breach. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002"},{"s":1639448400,"t":"Weather for the Week Ahead","audioDescription":false,"m":[27171,300,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":64799,"d":"Detailed weather forecast. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002"}]}}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epgservices.sky.com/5.2.2/api/2.0/channel/json/2002/1639526400/86400/4' - ) -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-12-14T01:50:00.000Z', - stop: '2021-12-14T02:20:00.000Z', - title: 'Question of Sport', - icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002' - }, - { - start: '2021-12-14T02:20:00.000Z', - stop: '2021-12-14T02:25:00.000Z', - title: 'Weather for the Week Ahead', - icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"listings":{"2002":[]}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sky.com + +const { parser, url } = require('./sky.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-12-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2002', + xmltv_id: 'BBCOneLondon.uk' +} +const content = + '{"listings":{"2002":[{"s":1639446600,"t":"Question of Sport","audioDescription":false,"m":[27170,1800,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":53228,"d":"14/36. In this Christmas special, Paddy, Sam and Ugo are joined by Anton Du Beke, Shaun Wallace, Big Zuu and Jules Breach. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002"},{"s":1639448400,"t":"Weather for the Week Ahead","audioDescription":false,"m":[27171,300,1,1,"--"],"rr":"S","subtitleHearing":true,"sid":64799,"d":"Detailed weather forecast. Also in HD. [S]","img":"lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002"}]}}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epgservices.sky.com/5.2.2/api/2.0/channel/json/2002/1639526400/86400/4' + ) +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-12-14T01:50:00.000Z', + stop: '2021-12-14T02:20:00.000Z', + title: 'Question of Sport', + icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/7f80ef03-3d8a-4f73-bf7d-6b03f410c7a8/2002' + }, + { + start: '2021-12-14T02:20:00.000Z', + stop: '2021-12-14T02:25:00.000Z', + title: 'Weather for the Week Ahead', + icon: 'http://epgstatic.sky.com/epgdata/1.0/paimage/46/1/lisa/5.2.2/linear/channel/8fcf08b7-4081-499a-bf63-d100908e2d75/2002' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"listings":{"2002":[]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sky.de/sky.de.config.js b/sites/sky.de/sky.de.config.js index 26a14e0c..fc1a98e8 100644 --- a/sites/sky.de/sky.de.config.js +++ b/sites/sky.de/sky.de.config.js @@ -1,50 +1,50 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'sky.de', - days: 2, - url: 'https://www.sky.de/sgtvg/service/getBroadcastsForGrid', - request: { - method: 'POST', - headers: { - 'accept-language': 'en-GB', - 'accept-encoding': 'gzip, deflate, br', - accept: 'application/json' - }, - data: function ({ channel, date }) { - return { - cil: [channel.site_id], - d: date.valueOf() - } - } - }, - parser: function ({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.et, - description: item.epit, - category: item.ec, - start: dayjs(item.bsdt), - stop: dayjs(item.bedt), - season: item.sn, - episode: item.en, - icon: item.pu ? `http://sky.de${item.pu}` : null - }) - }) - - return programs - } -} - -function parseContent(content, channel) { - const json = JSON.parse(content) - if (!Array.isArray(json.cl)) return null - return json.cl.find(i => i.ci == channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - return data && Array.isArray(data.el) ? data.el : [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'sky.de', + days: 2, + url: 'https://www.sky.de/sgtvg/service/getBroadcastsForGrid', + request: { + method: 'POST', + headers: { + 'accept-language': 'en-GB', + 'accept-encoding': 'gzip, deflate, br', + accept: 'application/json' + }, + data: function ({ channel, date }) { + return { + cil: [channel.site_id], + d: date.valueOf() + } + } + }, + parser: function ({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.et, + description: item.epit, + category: item.ec, + start: dayjs(item.bsdt), + stop: dayjs(item.bedt), + season: item.sn, + episode: item.en, + icon: item.pu ? `http://sky.de${item.pu}` : null + }) + }) + + return programs + } +} + +function parseContent(content, channel) { + const json = JSON.parse(content) + if (!Array.isArray(json.cl)) return null + return json.cl.find(i => i.ci == channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + return data && Array.isArray(data.el) ? data.el : [] +} diff --git a/sites/sky.de/sky.de.test.js b/sites/sky.de/sky.de.test.js index 75217964..c91faabb 100644 --- a/sites/sky.de/sky.de.test.js +++ b/sites/sky.de/sky.de.test.js @@ -1,68 +1,68 @@ -// npm run grab -- --site=sky.de - -const { parser, url, request } = require('./sky.de.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-02-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '522', - xmltv_id: 'WarnerTVComedyHD.de' -} - -const content = - '{"cl":[{"ci":522,"el":[{"ei":122309300,"bsdt":1645916700000,"bst":"00:05","bedt":1645918200000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Der Experte","sn":"4","en":"11","pu":"/static/img/program_guide/1522936_s.jpg"},{"ei":122309301,"bsdt":1645918200000,"bst":"00:30","bedt":1645919700000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Speedy Gonzales","sn":"4","en":"12","pu":"/static/img/program_guide/1522937_s.jpg"}]}]}' - -it('can generate valid url', () => { - expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - cil: [channel.site_id], - d: date.valueOf() - }) -}) - -it('can parse response', () => { - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'King of Queens', - description: 'Der Experte', - category: 'Comedyserie', - start: '2022-02-26T23:05:00.000Z', - stop: '2022-02-26T23:30:00.000Z', - season: '4', - episode: '11', - icon: 'http://sky.de/static/img/program_guide/1522936_s.jpg' - }, - { - title: 'King of Queens', - description: 'Speedy Gonzales', - category: 'Comedyserie', - start: '2022-02-26T23:30:00.000Z', - stop: '2022-02-26T23:55:00.000Z', - season: '4', - episode: '12', - icon: 'http://sky.de/static/img/program_guide/1522937_s.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sky.de + +const { parser, url, request } = require('./sky.de.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-02-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '522', + xmltv_id: 'WarnerTVComedyHD.de' +} + +const content = + '{"cl":[{"ci":522,"el":[{"ei":122309300,"bsdt":1645916700000,"bst":"00:05","bedt":1645918200000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Der Experte","sn":"4","en":"11","pu":"/static/img/program_guide/1522936_s.jpg"},{"ei":122309301,"bsdt":1645918200000,"bst":"00:30","bedt":1645919700000,"len":25,"et":"King of Queens","ec":"Comedyserie","cop":"USA","yop":2001,"fsk":"ab 0 Jahre","epit":"Speedy Gonzales","sn":"4","en":"12","pu":"/static/img/program_guide/1522937_s.jpg"}]}]}' + +it('can generate valid url', () => { + expect(url).toBe('https://www.sky.de/sgtvg/service/getBroadcastsForGrid') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + cil: [channel.site_id], + d: date.valueOf() + }) +}) + +it('can parse response', () => { + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'King of Queens', + description: 'Der Experte', + category: 'Comedyserie', + start: '2022-02-26T23:05:00.000Z', + stop: '2022-02-26T23:30:00.000Z', + season: '4', + episode: '11', + icon: 'http://sky.de/static/img/program_guide/1522936_s.jpg' + }, + { + title: 'King of Queens', + description: 'Speedy Gonzales', + category: 'Comedyserie', + start: '2022-02-26T23:30:00.000Z', + stop: '2022-02-26T23:55:00.000Z', + season: '4', + episode: '12', + icon: 'http://sky.de/static/img/program_guide/1522937_s.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/sportsnet.ca/sportsnet.ca.config.js b/sites/sportsnet.ca/sportsnet.ca.config.js index 247902b1..deed0683 100644 --- a/sites/sportsnet.ca/sportsnet.ca.config.js +++ b/sites/sportsnet.ca/sportsnet.ca.config.js @@ -1,50 +1,50 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'sportsnet.ca', - days: 2, - url: function ({ channel, date }) { - return `https://production-cdn.sportsnet.ca/api/schedules?channels=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}&duration=24&hour=0` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.item.title, - description: item.item.shortDescription, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseIcon(item) { - if (!item.item || !item.item.images) return null - - return item.item.images.tile -} - -function parseStart(item) { - return dayjs.utc(item.startDate) -} - -function parseStop(item) { - return dayjs.utc(item.endDate) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!Array.isArray(data) || !Array.isArray(data[0].schedules)) return [] - - return data[0].schedules -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'sportsnet.ca', + days: 2, + url: function ({ channel, date }) { + return `https://production-cdn.sportsnet.ca/api/schedules?channels=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}&duration=24&hour=0` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.item.title, + description: item.item.shortDescription, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseIcon(item) { + if (!item.item || !item.item.images) return null + + return item.item.images.tile +} + +function parseStart(item) { + return dayjs.utc(item.startDate) +} + +function parseStop(item) { + return dayjs.utc(item.endDate) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!Array.isArray(data) || !Array.isArray(data[0].schedules)) return [] + + return data[0].schedules +} diff --git a/sites/sportsnet.ca/sportsnet.ca.test.js b/sites/sportsnet.ca/sportsnet.ca.test.js index 9b00e5e8..49efa7d9 100644 --- a/sites/sportsnet.ca/sportsnet.ca.test.js +++ b/sites/sportsnet.ca/sportsnet.ca.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=sportsnet.ca - -const { parser, url } = require('./sportsnet.ca.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '24533', - xmltv_id: 'SportsNetOntario.ca' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://production-cdn.sportsnet.ca/api/schedules?channels=24533&date=2022-03-14&duration=24&hour=0' - ) -}) - -it('can parse response', () => { - const content = - '[{"channelId":"24533","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[{"channelId":"24533","customFields":{"ContentId":"EP029977175139","Checksum":"2DA90E7E66B9C311F98B186B89C50FAD"},"endDate":"2022-03-14T02:30:00Z","id":"826cb731-9de4-4cf3-bcca-d548d8a33d16","startDate":"2022-03-14T00:00:00Z","item":{"id":"34a028b0-eacf-40f3-9bf9-62ee3330df1b","type":"program","title":"Calgary Flames at Colorado Avalanche","shortDescription":"Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.","path":"/channel/24533","duration":9000,"images":{"tile":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785305\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160","wallpaper":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785311\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160"}}}]}]' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-14T00:00:00.000Z', - stop: '2022-03-14T02:30:00.000Z', - title: 'Calgary Flames at Colorado Avalanche', - description: - 'Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.', - icon: "https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='785305'&EntityType='LinearSchedule'&EntityId='826cb731-9de4-4cf3-bcca-d548d8a33d16'&Width=3840&Height=2160" - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '[{"channelId":"245321","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[]}]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=sportsnet.ca + +const { parser, url } = require('./sportsnet.ca.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '24533', + xmltv_id: 'SportsNetOntario.ca' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://production-cdn.sportsnet.ca/api/schedules?channels=24533&date=2022-03-14&duration=24&hour=0' + ) +}) + +it('can parse response', () => { + const content = + '[{"channelId":"24533","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[{"channelId":"24533","customFields":{"ContentId":"EP029977175139","Checksum":"2DA90E7E66B9C311F98B186B89C50FAD"},"endDate":"2022-03-14T02:30:00Z","id":"826cb731-9de4-4cf3-bcca-d548d8a33d16","startDate":"2022-03-14T00:00:00Z","item":{"id":"34a028b0-eacf-40f3-9bf9-62ee3330df1b","type":"program","title":"Calgary Flames at Colorado Avalanche","shortDescription":"Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.","path":"/channel/24533","duration":9000,"images":{"tile":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785305\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160","wallpaper":"https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format=\'jpg\'&Quality=85&ImageId=\'785311\'&EntityType=\'LinearSchedule\'&EntityId=\'826cb731-9de4-4cf3-bcca-d548d8a33d16\'&Width=3840&Height=2160"}}}]}]' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-14T00:00:00.000Z', + stop: '2022-03-14T02:30:00.000Z', + title: 'Calgary Flames at Colorado Avalanche', + description: + 'Johnny Gaudreau and the Flames pay a visit to the Avalanche. Calgary won 4-3 in overtime March 5.', + icon: "https://production-static.sportsnet-static.com/shain/v1/dataservice/ResizeImage/$value?Format='jpg'&Quality=85&ImageId='785305'&EntityType='LinearSchedule'&EntityId='826cb731-9de4-4cf3-bcca-d548d8a33d16'&Width=3840&Height=2160" + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '[{"channelId":"245321","startDate":"2022-03-14T00:00:00.000Z","endDate":"2022-03-15T00:00:00.000Z","schedules":[]}]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/starhubtvplus.com/starhubtvplus.com.config.js b/sites/starhubtvplus.com/starhubtvplus.com.config.js index 66a78cea..3fe6553a 100644 --- a/sites/starhubtvplus.com/starhubtvplus.com.config.js +++ b/sites/starhubtvplus.com/starhubtvplus.com.config.js @@ -1,85 +1,85 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const APP_KEY = '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc' -const SESSION_KEY = '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' - -module.exports = { - site: 'starhubtvplus.com', - days: 2, - request: { - headers: { - 'x-application-key': APP_KEY, - 'x-application-session': SESSION_KEY - }, - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - url: function ({ date }) { - const variables = JSON.stringify({ - category: '', - dateFrom: date.format('YYYY-MM-DD'), - dateTo: date.add(1, 'd').format('YYYY-MM-DD') - }) - const query = - 'query webFilteredEpg($category: String, $dateFrom: DateWithoutTime, $dateTo: DateWithoutTime!) { nagraEpg(category: $category) { items { id: tvChannel image name: longName programs: programsByDate(dateFrom: $dateFrom, dateTo: $dateTo) { id title description Categories startTime endTime }}}}' - - const params = `operationName=webFilteredEpg&variables=${encodeURIComponent( - variables - )}&query=${encodeURIComponent(query)}` - - return `https://api.starhubtvplus.com/epg?${params}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - category: item.Categories, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get( - 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22,%22dateFrom%22%3A%222022-05-10%22,%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20%7D%7D%7D', - { - headers: { - 'x-application-key': APP_KEY, - 'x-application-session': SESSION_KEY - } - } - ) - .then(r => r.data.data.nagraEpg.items) - .catch(console.log) - - return items.map(item => ({ - site_id: item.id, - name: item.name.replace('_DASH', '') - })) - } -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !data.data || !data.data.nagraEpg || !Array.isArray(data.data.nagraEpg.items)) - return [] - const ch = data.data.nagraEpg.items.find(ch => ch.id == channel.site_id) - - return ch && Array.isArray(ch.programs) ? ch.programs : [] -} +const axios = require('axios') +const dayjs = require('dayjs') + +const APP_KEY = '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc' +const SESSION_KEY = '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' + +module.exports = { + site: 'starhubtvplus.com', + days: 2, + request: { + headers: { + 'x-application-key': APP_KEY, + 'x-application-session': SESSION_KEY + }, + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + url: function ({ date }) { + const variables = JSON.stringify({ + category: '', + dateFrom: date.format('YYYY-MM-DD'), + dateTo: date.add(1, 'd').format('YYYY-MM-DD') + }) + const query = + 'query webFilteredEpg($category: String, $dateFrom: DateWithoutTime, $dateTo: DateWithoutTime!) { nagraEpg(category: $category) { items { id: tvChannel image name: longName programs: programsByDate(dateFrom: $dateFrom, dateTo: $dateTo) { id title description Categories startTime endTime }}}}' + + const params = `operationName=webFilteredEpg&variables=${encodeURIComponent( + variables + )}&query=${encodeURIComponent(query)}` + + return `https://api.starhubtvplus.com/epg?${params}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + category: item.Categories, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get( + 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22,%22dateFrom%22%3A%222022-05-10%22,%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20%7D%7D%7D', + { + headers: { + 'x-application-key': APP_KEY, + 'x-application-session': SESSION_KEY + } + } + ) + .then(r => r.data.data.nagraEpg.items) + .catch(console.log) + + return items.map(item => ({ + site_id: item.id, + name: item.name.replace('_DASH', '') + })) + } +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !data.data || !data.data.nagraEpg || !Array.isArray(data.data.nagraEpg.items)) + return [] + const ch = data.data.nagraEpg.items.find(ch => ch.id == channel.site_id) + + return ch && Array.isArray(ch.programs) ? ch.programs : [] +} diff --git a/sites/starhubtvplus.com/starhubtvplus.com.test.js b/sites/starhubtvplus.com/starhubtvplus.com.test.js index ac37133b..7819a9aa 100644 --- a/sites/starhubtvplus.com/starhubtvplus.com.test.js +++ b/sites/starhubtvplus.com/starhubtvplus.com.test.js @@ -1,63 +1,63 @@ -// npm run channels:parse -- --config=sites/starhubtvplus.com/starhubtvplus.com.config.js --output=sites/starhubtvplus.com/starhubtvplus.com.channels.xml -// npm run grab -- --site=starhubtvplus.com - -const { parser, url, request } = require('./starhubtvplus.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '102', - xmltv_id: 'Channel5.sg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22%2C%22dateFrom%22%3A%222022-05-10%22%2C%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String%2C%20%24dateFrom%3A%20DateWithoutTime%2C%20%24dateTo%3A%20DateWithoutTime!)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20programs%3A%20programsByDate(dateFrom%3A%20%24dateFrom%2C%20dateTo%3A%20%24dateTo)%20%7B%20id%20title%20description%20Categories%20startTime%20endTime%20%7D%7D%7D%7D' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-application-key': '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc', - 'x-application-session': '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' - }) -}) - -it('can generate valid cache settings', () => { - expect(request.cache).toMatchObject({ - ttl: 60 * 60 * 1000 - }) -}) - -it('can parse response', () => { - const content = - '{"data":{"nagraEpg":{"items":[{"id":102,"name":"Channel 5 HD_DASH","programs":[{"id":"GLOBAL_TC0021650123","title":"Luke Nguyen\'s Vietnam","description":"Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.","Categories":["Others"],"startTime":1652110200000,"endTime":1652112000000}]}]}}}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-05-09T15:30:00.000Z', - stop: '2022-05-09T16:00:00.000Z', - title: "Luke Nguyen's Vietnam", - description: - 'Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.', - category: ['Others'] - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '{"errors":[{"code":"A9999","message":"Syntax, request headers or server error","extendedLogging":{"message":"Cannot read property \'operation\' of undefined"}}]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/starhubtvplus.com/starhubtvplus.com.config.js --output=sites/starhubtvplus.com/starhubtvplus.com.channels.xml +// npm run grab -- --site=starhubtvplus.com + +const { parser, url, request } = require('./starhubtvplus.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '102', + xmltv_id: 'Channel5.sg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.starhubtvplus.com/epg?operationName=webFilteredEpg&variables=%7B%22category%22%3A%22%22%2C%22dateFrom%22%3A%222022-05-10%22%2C%22dateTo%22%3A%222022-05-11%22%7D&query=query%20webFilteredEpg(%24category%3A%20String%2C%20%24dateFrom%3A%20DateWithoutTime%2C%20%24dateTo%3A%20DateWithoutTime!)%20%7B%20nagraEpg(category%3A%20%24category)%20%7B%20items%20%7B%20id%3A%20tvChannel%20image%20name%3A%20longName%20programs%3A%20programsByDate(dateFrom%3A%20%24dateFrom%2C%20dateTo%3A%20%24dateTo)%20%7B%20id%20title%20description%20Categories%20startTime%20endTime%20%7D%7D%7D%7D' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-application-key': '5ee2ef931de1c4001b2e7fa3_5ee2ec25a0e845001c1783dc', + 'x-application-session': '01G2QG0N3RWDNCBA1S5MK1MD2K17CE4431A2' + }) +}) + +it('can generate valid cache settings', () => { + expect(request.cache).toMatchObject({ + ttl: 60 * 60 * 1000 + }) +}) + +it('can parse response', () => { + const content = + '{"data":{"nagraEpg":{"items":[{"id":102,"name":"Channel 5 HD_DASH","programs":[{"id":"GLOBAL_TC0021650123","title":"Luke Nguyen\'s Vietnam","description":"Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.","Categories":["Others"],"startTime":1652110200000,"endTime":1652112000000}]}]}}}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-05-09T15:30:00.000Z', + stop: '2022-05-09T16:00:00.000Z', + title: "Luke Nguyen's Vietnam", + description: + 'Luke leaves the hustle and bustle of Hanoi behind for the mystical mountains of Sapa. There, he prepares some black chicken in and amongst the local streets. He cooks buffalo for a salad in the busy Sapa markets, as well as a tofu-and-tomato dish high up in the rice paddy fields with the most spectacular backdrop.', + category: ['Others'] + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '{"errors":[{"code":"A9999","message":"Syntax, request headers or server error","extendedLogging":{"message":"Cannot read property \'operation\' of undefined"}}]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/startimestv.com/startimestv.com.config.js b/sites/startimestv.com/startimestv.com.config.js index 42f255f9..8944afc8 100644 --- a/sites/startimestv.com/startimestv.com.config.js +++ b/sites/startimestv.com/startimestv.com.config.js @@ -1,115 +1,115 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'startimestv.com', - days: 2, - url: function ({ channel, date }) { - return `https://www.startimestv.com/channeldetail/${channel.site_id}/${date.format( - 'YYYY-MM-DD' - )}.html` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - programs.push({ - title: parseTitle($item), - season: parseSeason($item), - episode: parseEpisode($item), - description: parseDescription($item), - start: parseStart($item, date), - stop: parseStop($item, date) - }) - }) - - return programs - }, - async channels({ country }) { - const area = { - ke: 6, - ng: 2, - tz: 3, - ug: 4, - rw: 5, - gh: 32, - mw: 14, - ci: 22, - gn: 12, - bi: 9, - cg: 16, - cd: 11, - mg: 13, - mz: 15, - cm: 20, - ga: 19 - } - const data = await axios - .get('https://www.startimestv.com/tv_guide.html', { - headers: { - Cookie: `default_areaID=${area[country]}` - } - }) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - const script = $('body > script:nth-child(10)').html() - let [, json] = script.match(/var obj = eval\( '(.*)' \);/) || [null, null] - json = json.replace(/\\'/g, '') - const items = JSON.parse(json) - - return items.map(i => ({ - name: i.name, - site_id: i.id - })) - } -} - -function parseStart($item, date) { - const time = $item('.in > .t').text() - const [, HH, mm] = time.match(/^(\d{2}):(\d{2})/) || [null, null, null] - - return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null -} - -function parseStop($item, date) { - const time = $item('.in > .t').text() - const [, HH, mm] = time.match(/(\d{2}):(\d{2})$/) || [null, null, null] - - return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null -} - -function parseSeason($item) { - const title = parseTitle($item) - const [, season] = title.match(/ S(\d+)/) || [null, null] - - return season ? parseInt(season) : null -} - -function parseEpisode($item) { - const title = parseTitle($item) - const [, episode] = title.match(/ E(\d+)/) || [null, null] - - return episode ? parseInt(episode) : null -} - -function parseTitle($item) { - return $item('.in > h3').text() -} - -function parseDescription($item) { - return $item('.in > p').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('div.tv_gui > div.list > div > div').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'startimestv.com', + days: 2, + url: function ({ channel, date }) { + return `https://www.startimestv.com/channeldetail/${channel.site_id}/${date.format( + 'YYYY-MM-DD' + )}.html` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + programs.push({ + title: parseTitle($item), + season: parseSeason($item), + episode: parseEpisode($item), + description: parseDescription($item), + start: parseStart($item, date), + stop: parseStop($item, date) + }) + }) + + return programs + }, + async channels({ country }) { + const area = { + ke: 6, + ng: 2, + tz: 3, + ug: 4, + rw: 5, + gh: 32, + mw: 14, + ci: 22, + gn: 12, + bi: 9, + cg: 16, + cd: 11, + mg: 13, + mz: 15, + cm: 20, + ga: 19 + } + const data = await axios + .get('https://www.startimestv.com/tv_guide.html', { + headers: { + Cookie: `default_areaID=${area[country]}` + } + }) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + const script = $('body > script:nth-child(10)').html() + let [, json] = script.match(/var obj = eval\( '(.*)' \);/) || [null, null] + json = json.replace(/\\'/g, '') + const items = JSON.parse(json) + + return items.map(i => ({ + name: i.name, + site_id: i.id + })) + } +} + +function parseStart($item, date) { + const time = $item('.in > .t').text() + const [, HH, mm] = time.match(/^(\d{2}):(\d{2})/) || [null, null, null] + + return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null +} + +function parseStop($item, date) { + const time = $item('.in > .t').text() + const [, HH, mm] = time.match(/(\d{2}):(\d{2})$/) || [null, null, null] + + return HH && mm ? dayjs.utc(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm') : null +} + +function parseSeason($item) { + const title = parseTitle($item) + const [, season] = title.match(/ S(\d+)/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseEpisode($item) { + const title = parseTitle($item) + const [, episode] = title.match(/ E(\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseTitle($item) { + return $item('.in > h3').text() +} + +function parseDescription($item) { + return $item('.in > p').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('div.tv_gui > div.list > div > div').toArray() +} diff --git a/sites/startimestv.com/startimestv.com.test.js b/sites/startimestv.com/startimestv.com.test.js index 2ac19623..a11aecda 100644 --- a/sites/startimestv.com/startimestv.com.test.js +++ b/sites/startimestv.com/startimestv.com.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/startimestv.com/startimestv.com.config.js --output=./sites/startimestv.com/startimestv.com.channels.xml --set=country:ke -// npm run grab -- --site=startimestv.com - -const { parser, url } = require('./startimestv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-04-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1023102509', - xmltv_id: 'ZeeOneAfrica.za' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.startimestv.com/channeldetail/1023102509/2022-04-10.html' - ) -}) - -it('can parse response', () => { - const content = - '

    Guddan S2 E77

    00:00-01:00

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    00:00-01:00 Guddan S2 E77

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    ' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-10T00:00:00.000Z', - stop: '2022-04-10T01:00:00.000Z', - title: 'Guddan S2 E77', - season: 2, - episode: 77, - description: 'Vickrant is overjoyed to see Akshat in pain and not knowing what to do.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '

    Rate:

    Category:


    ' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/startimestv.com/startimestv.com.config.js --output=./sites/startimestv.com/startimestv.com.channels.xml --set=country:ke +// npm run grab -- --site=startimestv.com + +const { parser, url } = require('./startimestv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-04-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1023102509', + xmltv_id: 'ZeeOneAfrica.za' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.startimestv.com/channeldetail/1023102509/2022-04-10.html' + ) +}) + +it('can parse response', () => { + const content = + '

    Guddan S2 E77

    00:00-01:00

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    00:00-01:00 Guddan S2 E77

    Vickrant is overjoyed to see Akshat in pain and not knowing what to do.

    ' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-10T00:00:00.000Z', + stop: '2022-04-10T01:00:00.000Z', + title: 'Guddan S2 E77', + season: 2, + episode: 77, + description: 'Vickrant is overjoyed to see Akshat in pain and not knowing what to do.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '

    Rate:

    Category:


    ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/startv.com/startv.com.config.js b/sites/startv.com/startv.com.config.js index 25fa1fbb..a31b0c1c 100644 --- a/sites/startv.com/startv.com.config.js +++ b/sites/startv.com/startv.com.config.js @@ -1,88 +1,88 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://www.startv.com/umbraco/api/startvguideproxy' - -module.exports = { - site: 'startv.com', - days: 2, - url: `${API_ENDPOINT}/GetTvGuideSchedule`, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8' - }, - data({ channel, date }) { - return { - Channels: channel.site_id, - Start: date.format('YYYYMMDDHHmm'), - Stop: date.add(1, 'd').format('YYYYMMDDHHmm') - } - } - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.desc, - icon: item.programmeurl, - category: item.subgenre, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .post( - `${API_ENDPOINT}/GetChannelResult`, - { Genre: 'All Channels' }, - { - headers: { - 'Content-Type': 'application/json; charset=UTF-8' - } - } - ) - .then(r => JSON.parse(r.data)) - .catch(console.log) - - const channels = data.channelsbygenreandlanguage.channellist.channelnames.split(',') - return channels.map(item => { - return { - lang: 'hi', - site_id: item, - name: item - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.start, 'YYYYMMDDHHmm', 'Asia/Kolkata') -} - -function parseStop(item) { - return dayjs.tz(item.stop, 'YYYYMMDDHHmm', 'Asia/Kolkata') -} - -function parseItems(content, channel) { - if (!content.length) return [] - const json = JSON.parse(content) - if (!json.length) return [] - const data = JSON.parse(json) - if (!data || !data.ScheduleGrid || !Array.isArray(data.ScheduleGrid.channel)) return [] - const channelData = data.ScheduleGrid.channel.find(c => c.channeldisplayname === channel.site_id) - - return channelData && Array.isArray(channelData.programme) ? channelData.programme : [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://www.startv.com/umbraco/api/startvguideproxy' + +module.exports = { + site: 'startv.com', + days: 2, + url: `${API_ENDPOINT}/GetTvGuideSchedule`, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8' + }, + data({ channel, date }) { + return { + Channels: channel.site_id, + Start: date.format('YYYYMMDDHHmm'), + Stop: date.add(1, 'd').format('YYYYMMDDHHmm') + } + } + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.desc, + icon: item.programmeurl, + category: item.subgenre, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .post( + `${API_ENDPOINT}/GetChannelResult`, + { Genre: 'All Channels' }, + { + headers: { + 'Content-Type': 'application/json; charset=UTF-8' + } + } + ) + .then(r => JSON.parse(r.data)) + .catch(console.log) + + const channels = data.channelsbygenreandlanguage.channellist.channelnames.split(',') + return channels.map(item => { + return { + lang: 'hi', + site_id: item, + name: item + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.start, 'YYYYMMDDHHmm', 'Asia/Kolkata') +} + +function parseStop(item) { + return dayjs.tz(item.stop, 'YYYYMMDDHHmm', 'Asia/Kolkata') +} + +function parseItems(content, channel) { + if (!content.length) return [] + const json = JSON.parse(content) + if (!json.length) return [] + const data = JSON.parse(json) + if (!data || !data.ScheduleGrid || !Array.isArray(data.ScheduleGrid.channel)) return [] + const channelData = data.ScheduleGrid.channel.find(c => c.channeldisplayname === channel.site_id) + + return channelData && Array.isArray(channelData.programme) ? channelData.programme : [] +} diff --git a/sites/startv.com/startv.com.test.js b/sites/startv.com/startv.com.test.js index 7482f8c7..e39b5fa3 100644 --- a/sites/startv.com/startv.com.test.js +++ b/sites/startv.com/startv.com.test.js @@ -1,66 +1,66 @@ -// npm run channels:parse -- --config=sites/startv.com/startv.com.config.js --output=sites/startv.com/startv.com.channels.xml -// npm run grab -- --site=startv.com - -const { parser, url, request } = require('./startv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'STAR PLUS', - xmltv_id: 'StarPlus.in' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.startv.com/umbraco/api/startvguideproxy/GetTvGuideSchedule') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ channel, date })).toMatchObject({ - Channels: 'STAR PLUS', - Start: '202203310000', - Stop: '202204010000' - }) -}) - -it('can parse response', () => { - const content = - '"{\\"ScheduleGrid\\":{\\"channel\\":[{\\"id\\":null,\\"displayname\\":null,\\"channelid\\":\\"10000000000080000\\",\\"channellogourl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/channel/landscape/100x75/wHtcYVRZ.png\\",\\"channelgenre\\":\\"Hindi Entertainment\\",\\"channelweburl\\":\\"\\",\\"channeldisplayname\\":\\"STAR PLUS\\",\\"lcn\\":\\"1\\",\\"isfav\\":\\"0\\",\\"programme\\":[{\\"programmeid\\":\\"30000000550792674\\",\\"title\\":\\"Imlie\\",\\"start\\":\\"202203310000\\",\\"stop\\":\\"202203310030\\",\\"desc\\":\\"Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.\\",\\"programmeurl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg\\",\\"channelid\\":\\"10000000000080000\\",\\"date\\":\\"20220331\\",\\"episodenum\\":null,\\"subtitle\\":null,\\"scheduleid\\":\\"10000069158583187\\",\\"genre\\":\\"TV Show\\",\\"subgenre\\":\\"Drama\\",\\"programmescore\\":\\"0.083309\\",\\"languagename\\":\\"Hindi\\",\\"dubbedlanguageid\\":\\"10000000000040000\\",\\"timestring\\":\\"12:00 AM, Tomorrow\\",\\"duration\\":\\"30\\",\\"episodeshorttitle\\":\\"\\"}]}]}}"' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-30T18:30:00.000Z', - stop: '2022-03-30T19:00:00.000Z', - title: 'Imlie', - description: - 'Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.', - icon: 'http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg', - category: 'Drama' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '""' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/startv.com/startv.com.config.js --output=sites/startv.com/startv.com.channels.xml +// npm run grab -- --site=startv.com + +const { parser, url, request } = require('./startv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'STAR PLUS', + xmltv_id: 'StarPlus.in' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.startv.com/umbraco/api/startvguideproxy/GetTvGuideSchedule') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ channel, date })).toMatchObject({ + Channels: 'STAR PLUS', + Start: '202203310000', + Stop: '202204010000' + }) +}) + +it('can parse response', () => { + const content = + '"{\\"ScheduleGrid\\":{\\"channel\\":[{\\"id\\":null,\\"displayname\\":null,\\"channelid\\":\\"10000000000080000\\",\\"channellogourl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/channel/landscape/100x75/wHtcYVRZ.png\\",\\"channelgenre\\":\\"Hindi Entertainment\\",\\"channelweburl\\":\\"\\",\\"channeldisplayname\\":\\"STAR PLUS\\",\\"lcn\\":\\"1\\",\\"isfav\\":\\"0\\",\\"programme\\":[{\\"programmeid\\":\\"30000000550792674\\",\\"title\\":\\"Imlie\\",\\"start\\":\\"202203310000\\",\\"stop\\":\\"202203310030\\",\\"desc\\":\\"Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.\\",\\"programmeurl\\":\\"http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg\\",\\"channelid\\":\\"10000000000080000\\",\\"date\\":\\"20220331\\",\\"episodenum\\":null,\\"subtitle\\":null,\\"scheduleid\\":\\"10000069158583187\\",\\"genre\\":\\"TV Show\\",\\"subgenre\\":\\"Drama\\",\\"programmescore\\":\\"0.083309\\",\\"languagename\\":\\"Hindi\\",\\"dubbedlanguageid\\":\\"10000000000040000\\",\\"timestring\\":\\"12:00 AM, Tomorrow\\",\\"duration\\":\\"30\\",\\"episodeshorttitle\\":\\"\\"}]}]}}"' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-30T18:30:00.000Z', + stop: '2022-03-30T19:00:00.000Z', + title: 'Imlie', + description: + 'Imlie finds herself in deep trouble when she gets tied up before the wedding. Meanwhile, Aryan assumes that he is getting married to Imlie and performs the wedding rituals.', + icon: 'http://imagesstartv.whatsonindia.com/dasimages/landscape/360x270/59A9215E5DE13ABF4B05C59A6C87768AD61CA608M.jpg', + category: 'Drama' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '""' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/streamingtvguides.com/streamingtvguides.com.config.js b/sites/streamingtvguides.com/streamingtvguides.com.config.js index 4a14fdb1..6a6768d0 100644 --- a/sites/streamingtvguides.com/streamingtvguides.com.config.js +++ b/sites/streamingtvguides.com/streamingtvguides.com.config.js @@ -1,70 +1,70 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') -const _ = require('lodash') - -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -module.exports = { - site: 'streamingtvguides.com', - days: 2, - url({ channel }) { - return `https://streamingtvguides.com/Channel/${channel.site_id}` - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const start = parseStart($item) - if (!date.isSame(start, 'd')) return - - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop: parseStop($item) - }) - }) - - programs = _.orderBy(_.uniqBy(programs, 'start'), 'start') - - return programs - } -} - -function parseTitle($item) { - return $item('.card-body > .prog-contains > .card-title') - .clone() - .children() - .remove() - .end() - .text() - .trim() -} - -function parseDescription($item) { - return $item('.card-body > .card-text').clone().children().remove().end().text().trim() -} - -function parseStart($item) { - const date = $item('.card-body').clone().children().remove().end().text().trim() - const [time] = date.split(' - ') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() -} - -function parseStop($item) { - const date = $item('.card-body').clone().children().remove().end().text().trim() - const [, time] = date.split(' - ') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.container').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') +const _ = require('lodash') + +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +module.exports = { + site: 'streamingtvguides.com', + days: 2, + url({ channel }) { + return `https://streamingtvguides.com/Channel/${channel.site_id}` + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const start = parseStart($item) + if (!date.isSame(start, 'd')) return + + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop: parseStop($item) + }) + }) + + programs = _.orderBy(_.uniqBy(programs, 'start'), 'start') + + return programs + } +} + +function parseTitle($item) { + return $item('.card-body > .prog-contains > .card-title') + .clone() + .children() + .remove() + .end() + .text() + .trim() +} + +function parseDescription($item) { + return $item('.card-body > .card-text').clone().children().remove().end().text().trim() +} + +function parseStart($item) { + const date = $item('.card-body').clone().children().remove().end().text().trim() + const [time] = date.split(' - ') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() +} + +function parseStop($item) { + const date = $item('.card-body').clone().children().remove().end().text().trim() + const [, time] = date.split(' - ') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss [PST]', 'PST').utc() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.container').toArray() +} diff --git a/sites/streamingtvguides.com/streamingtvguides.com.test.js b/sites/streamingtvguides.com/streamingtvguides.com.test.js index 1d640096..fcf8fbc3 100644 --- a/sites/streamingtvguides.com/streamingtvguides.com.test.js +++ b/sites/streamingtvguides.com/streamingtvguides.com.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=streamingtvguides.com - -const { parser, url } = require('./streamingtvguides.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-27', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'GMAPNY', - xmltv_id: 'GMAPinoyTVUSACanada.ph' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://streamingtvguides.com/Channel/GMAPNY') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(38) - - expect(results[0]).toMatchObject({ - start: '2023-06-27T00:40:00.000Z', - stop: '2023-06-27T02:00:00.000Z', - title: '24 Oras', - description: 'Up to the minute news around the world.' - }) - - expect(results[37]).toMatchObject({ - start: '2023-06-27T21:50:00.000Z', - stop: '2023-06-28T00:00:00.000Z', - title: 'Eat Bulaga', - description: 'Rousing and engrossing segments with engaging hosts.' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - const result = parser({ - date, - content - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=streamingtvguides.com + +const { parser, url } = require('./streamingtvguides.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-27', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'GMAPNY', + xmltv_id: 'GMAPinoyTVUSACanada.ph' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://streamingtvguides.com/Channel/GMAPNY') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(38) + + expect(results[0]).toMatchObject({ + start: '2023-06-27T00:40:00.000Z', + stop: '2023-06-27T02:00:00.000Z', + title: '24 Oras', + description: 'Up to the minute news around the world.' + }) + + expect(results[37]).toMatchObject({ + start: '2023-06-27T21:50:00.000Z', + stop: '2023-06-28T00:00:00.000Z', + title: 'Eat Bulaga', + description: 'Rousing and engrossing segments with engaging hosts.' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + const result = parser({ + date, + content + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/superguidatv.it/superguidatv.it.config.js b/sites/superguidatv.it/superguidatv.it.config.js index b8d97142..6a89e9fe 100644 --- a/sites/superguidatv.it/superguidatv.it.config.js +++ b/sites/superguidatv.it/superguidatv.it.config.js @@ -1,117 +1,117 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'superguidatv.it', - days: 3, - url({ channel, date }) { - let diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') - let day = { - 0: 'oggi', - 1: 'domani', - 2: 'dopodomani' - } - - return `https://www.superguidatv.it/programmazione-canale/${day[diff]}/guida-programmi-tv-${channel.site_id}/` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - category: parseCategory($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const providers = [ - '', - 'premium/', - 'sky-intrattenimento/', - 'sky-sport/', - 'sky-cinema/', - 'sky-doc-e-lifestyle/', - 'sky-news/', - 'sky-bambini/', - 'sky-musica/', - 'sky-primafila/', - 'dazn/', - 'rsi/' - ] - const promises = providers.map(p => axios.get(`https://www.superguidatv.it/canali/${p}`)) - - const channels = [] - await Promise.all(promises) - .then(responses => { - responses.forEach(r => { - const $ = cheerio.load(r.data) - - $('.sgtvchannellist_mainContainer .sgtvchannel_divCell a').each((i, link) => { - let [, site_id] = $(link) - .attr('href') - .match(/guida-programmi-tv-(.*)\/$/) || [null, null] - let name = $(link).find('.pchannel').text().trim() - - channels.push({ - lang: 'it', - site_id, - name - }) - }) - }) - }) - .catch(console.log) - - return channels - } -} - -function parseStart($item, date) { - const hours = $item('.sgtvchannelplan_hoursCell') - .clone() - .children('.sgtvOnairSpan') - .remove() - .end() - .text() - .trim() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${hours}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Rome' - }).toUTC() -} - -function parseTitle($item) { - return $item('.sgtvchannelplan_spanInfoNextSteps').text().trim() -} - -function parseCategory($item) { - const eventType = $item('.sgtvchannelplan_spanEventType').text().trim() - const [, category] = eventType.match(/(^[^(]+)/) || [null, ''] - - return category.trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.sgtvchannelplan_divContainer > .sgtvchannelplan_divTableRow') - .has('#containerInfoEvent') - .toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'superguidatv.it', + days: 3, + url({ channel, date }) { + let diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') + let day = { + 0: 'oggi', + 1: 'domani', + 2: 'dopodomani' + } + + return `https://www.superguidatv.it/programmazione-canale/${day[diff]}/guida-programmi-tv-${channel.site_id}/` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + category: parseCategory($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const providers = [ + '', + 'premium/', + 'sky-intrattenimento/', + 'sky-sport/', + 'sky-cinema/', + 'sky-doc-e-lifestyle/', + 'sky-news/', + 'sky-bambini/', + 'sky-musica/', + 'sky-primafila/', + 'dazn/', + 'rsi/' + ] + const promises = providers.map(p => axios.get(`https://www.superguidatv.it/canali/${p}`)) + + const channels = [] + await Promise.all(promises) + .then(responses => { + responses.forEach(r => { + const $ = cheerio.load(r.data) + + $('.sgtvchannellist_mainContainer .sgtvchannel_divCell a').each((i, link) => { + let [, site_id] = $(link) + .attr('href') + .match(/guida-programmi-tv-(.*)\/$/) || [null, null] + let name = $(link).find('.pchannel').text().trim() + + channels.push({ + lang: 'it', + site_id, + name + }) + }) + }) + }) + .catch(console.log) + + return channels + } +} + +function parseStart($item, date) { + const hours = $item('.sgtvchannelplan_hoursCell') + .clone() + .children('.sgtvOnairSpan') + .remove() + .end() + .text() + .trim() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${hours}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Rome' + }).toUTC() +} + +function parseTitle($item) { + return $item('.sgtvchannelplan_spanInfoNextSteps').text().trim() +} + +function parseCategory($item) { + const eventType = $item('.sgtvchannelplan_spanEventType').text().trim() + const [, category] = eventType.match(/(^[^(]+)/) || [null, ''] + + return category.trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.sgtvchannelplan_divContainer > .sgtvchannelplan_divTableRow') + .has('#containerInfoEvent') + .toArray() +} diff --git a/sites/superguidatv.it/superguidatv.it.test.js b/sites/superguidatv.it/superguidatv.it.test.js index 38f32eda..0f8585e8 100644 --- a/sites/superguidatv.it/superguidatv.it.test.js +++ b/sites/superguidatv.it/superguidatv.it.test.js @@ -1,67 +1,67 @@ -// npm run channels:parse -- --config=sites/superguidatv.it/superguidatv.it.config.js --output=sites/superguidatv.it/superguidatv.it.channels.xml -// npm run grab -- --site=superguidatv.it - -const { parser, url } = require('./superguidatv.it.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'virgin-radio/461', - xmltv_id: 'VirginRadioTV.it' -} - -it('can generate valid url', () => { - expect(url({ channel, date: dayjs.utc().startOf('d') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/oggi/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can generate valid url for tomorrow', () => { - expect(url({ channel, date: dayjs.utc().startOf('d').add(1, 'd') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/domani/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can generate valid url for after tomorrow', () => { - expect(url({ channel, date: dayjs.utc().startOf('d').add(2, 'd') })).toBe( - 'https://www.superguidatv.it/programmazione-canale/dopodomani/guida-programmi-tv-virgin-radio/461/' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-11T01:00:00.000Z', - stop: '2023-01-11T05:00:00.000Z', - title: 'All Nite Rock', - category: 'Musica' - }) - - expect(results[13]).toMatchObject({ - start: '2023-01-12T05:00:00.000Z', - stop: '2023-01-12T05:30:00.000Z', - title: 'Free Rock', - category: 'Musica' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/superguidatv.it/superguidatv.it.config.js --output=sites/superguidatv.it/superguidatv.it.channels.xml +// npm run grab -- --site=superguidatv.it + +const { parser, url } = require('./superguidatv.it.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'virgin-radio/461', + xmltv_id: 'VirginRadioTV.it' +} + +it('can generate valid url', () => { + expect(url({ channel, date: dayjs.utc().startOf('d') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/oggi/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can generate valid url for tomorrow', () => { + expect(url({ channel, date: dayjs.utc().startOf('d').add(1, 'd') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/domani/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can generate valid url for after tomorrow', () => { + expect(url({ channel, date: dayjs.utc().startOf('d').add(2, 'd') })).toBe( + 'https://www.superguidatv.it/programmazione-canale/dopodomani/guida-programmi-tv-virgin-radio/461/' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-11T01:00:00.000Z', + stop: '2023-01-11T05:00:00.000Z', + title: 'All Nite Rock', + category: 'Musica' + }) + + expect(results[13]).toMatchObject({ + start: '2023-01-12T05:00:00.000Z', + stop: '2023-01-12T05:30:00.000Z', + title: 'Free Rock', + category: 'Musica' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/taiwanplus.com/taiwanplus.com.config.js b/sites/taiwanplus.com/taiwanplus.com.config.js index 94ed17ae..745cae58 100644 --- a/sites/taiwanplus.com/taiwanplus.com.config.js +++ b/sites/taiwanplus.com/taiwanplus.com.config.js @@ -1,73 +1,73 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const isSameOrAfter = require('dayjs/plugin/isSameOrAfter') -const isSameOrBefore = require('dayjs/plugin/isSameOrBefore') - -dayjs.extend(utc) -dayjs.extend(isSameOrAfter) -dayjs.extend(isSameOrBefore) - -module.exports = { - site: 'taiwanplus.com', - days: 7, - output: 'taiwanplus.com.guide.xml', - channels: 'taiwanplus.com.channels.xml', - lang: 'en', - delay: 5000, - - url: function () { - return 'https://www.taiwanplus.com/api/video/live/schedule/0' - }, - - request: { - method: 'GET', - timeout: 5000, - cache: { ttl: 60 * 60 * 1000 }, // 60 * 60 seconds = 1 hour - headers: { - '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' - } - }, - - logo: function (context) { - return context.channel.logo - }, - - parser: function (context) { - const programs = [] - const scheduleDates = parseItems(context.content) - const today = dayjs.utc(context.date).startOf('day') - - for (let scheduleDate of scheduleDates) { - const currentScheduleDate = new dayjs.utc(scheduleDate.date, 'YYYY/MM/DD') - - if (currentScheduleDate.isSame(today)) { - scheduleDate.schedule.forEach(function (program, i) { - programs.push({ - title: program.title, - start: dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm'), - stop: - i != scheduleDate.schedule.length - 1 - ? dayjs.utc(scheduleDate.schedule[i + 1].dateTime, 'YYYY/MM/DD HH:mm') - : dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm').add(1, 'day').startOf('day'), - description: program.description, - icon: program.image, - category: program.categoryName, - rating: program.ageRating - }) - }) - } - } - - return programs - } -} - -function parseItems(content) { - if (content != '') { - const data = JSON.parse(content) - return !data || !data.data || !Array.isArray(data.data) ? [] : data.data - } else { - return [] - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const isSameOrAfter = require('dayjs/plugin/isSameOrAfter') +const isSameOrBefore = require('dayjs/plugin/isSameOrBefore') + +dayjs.extend(utc) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) + +module.exports = { + site: 'taiwanplus.com', + days: 7, + output: 'taiwanplus.com.guide.xml', + channels: 'taiwanplus.com.channels.xml', + lang: 'en', + delay: 5000, + + url: function () { + return 'https://www.taiwanplus.com/api/video/live/schedule/0' + }, + + request: { + method: 'GET', + timeout: 5000, + cache: { ttl: 60 * 60 * 1000 }, // 60 * 60 seconds = 1 hour + headers: { + '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' + } + }, + + logo: function (context) { + return context.channel.logo + }, + + parser: function (context) { + const programs = [] + const scheduleDates = parseItems(context.content) + const today = dayjs.utc(context.date).startOf('day') + + for (let scheduleDate of scheduleDates) { + const currentScheduleDate = new dayjs.utc(scheduleDate.date, 'YYYY/MM/DD') + + if (currentScheduleDate.isSame(today)) { + scheduleDate.schedule.forEach(function (program, i) { + programs.push({ + title: program.title, + start: dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm'), + stop: + i != scheduleDate.schedule.length - 1 + ? dayjs.utc(scheduleDate.schedule[i + 1].dateTime, 'YYYY/MM/DD HH:mm') + : dayjs.utc(program.dateTime, 'YYYY/MM/DD HH:mm').add(1, 'day').startOf('day'), + description: program.description, + icon: program.image, + category: program.categoryName, + rating: program.ageRating + }) + }) + } + } + + return programs + } +} + +function parseItems(content) { + if (content != '') { + const data = JSON.parse(content) + return !data || !data.data || !Array.isArray(data.data) ? [] : data.data + } else { + return [] + } +} diff --git a/sites/taiwanplus.com/taiwanplus.com.test.js b/sites/taiwanplus.com/taiwanplus.com.test.js index a38c74c0..dc714227 100644 --- a/sites/taiwanplus.com/taiwanplus.com.test.js +++ b/sites/taiwanplus.com/taiwanplus.com.test.js @@ -1,45 +1,45 @@ -// npm run grab -- --site=taiwanplus.com -// npx jest taiwanplus.com.test.js - -const { url, parser } = require('./taiwanplus.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2023-08-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '#', - xmltv_id: 'TaiwanPlusTV.tw', - lang: 'en', - logo: 'https://i.imgur.com/SfcZyqm.png' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0') -}) - -it('can parse response', () => { - const content = - '{"data":[{"date":"2023/08/20","weekday":"SUN","schedule":[{"programId":30668,"dateTime":"2023/08/20 00:00","time":"00:00","image":"https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp","title":"Master Class","shortDescription":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","description":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","ageRating":"0+","programWebSiteType":"4","url":"","vodId":null,"categoryId":90000474,"categoryType":2,"categoryName":"TaiwanPlus ✕ Discovery","categoryFullPath":"Originals/TaiwanPlus ✕ Discovery","encodedCategoryFullPath":"originals/taiwanplus-discovery"}]}],"success":true,"code":"0000","message":""}' - - const results = parser({ content, date }) - - expect(results).toMatchObject([ - { - title: 'Master Class', - start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'), - stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'), - description: - 'From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.', - icon: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp', - category: 'TaiwanPlus ✕ Discovery', - rating: '0+' - } - ]) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=taiwanplus.com +// npx jest taiwanplus.com.test.js + +const { url, parser } = require('./taiwanplus.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2023-08-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '#', + xmltv_id: 'TaiwanPlusTV.tw', + lang: 'en', + logo: 'https://i.imgur.com/SfcZyqm.png' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.taiwanplus.com/api/video/live/schedule/0') +}) + +it('can parse response', () => { + const content = + '{"data":[{"date":"2023/08/20","weekday":"SUN","schedule":[{"programId":30668,"dateTime":"2023/08/20 00:00","time":"00:00","image":"https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp","title":"Master Class","shortDescription":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","description":"From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.","ageRating":"0+","programWebSiteType":"4","url":"","vodId":null,"categoryId":90000474,"categoryType":2,"categoryName":"TaiwanPlus ✕ Discovery","categoryFullPath":"Originals/TaiwanPlus ✕ Discovery","encodedCategoryFullPath":"originals/taiwanplus-discovery"}]}],"success":true,"code":"0000","message":""}' + + const results = parser({ content, date }) + + expect(results).toMatchObject([ + { + title: 'Master Class', + start: dayjs.utc('2023/08/20 00:00', 'YYYY/MM/DD HH:mm'), + stop: dayjs.utc('2023/08/21 00:00', 'YYYY/MM/DD HH:mm'), + description: + 'From blockchain to Buddha statues, Taiwan’s culture is a kaleidoscope of old and new just waiting to be discovered.', + icon: 'https://prod-img.taiwanplus.com/live-schedule/Single/S30668_20230810104937.webp', + category: 'TaiwanPlus ✕ Discovery', + rating: '0+' + } + ]) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tapdmv.com/tapdmv.com.config.js b/sites/tapdmv.com/tapdmv.com.config.js index 1424a82c..388ac657 100644 --- a/sites/tapdmv.com/tapdmv.com.config.js +++ b/sites/tapdmv.com/tapdmv.com.config.js @@ -1,61 +1,61 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tapdmv.com', - days: 2, - url({ channel, date }) { - return `https://epg.tapdmv.com/calendar/${ - channel.site_id - }?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=${date.toJSON()}&end=${date - .add(1, 'd') - .toJSON()}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content, date) - items.forEach(item => { - programs.push({ - title: item.program.trim(), - description: item.description, - category: item.genre, - icon: item.thumbnailImage, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://epg.tapdmv.com/calendar?$limit=10000&$sort[createdAt]=-1') - .then(r => r.data.data) - .catch(console.log) - - return items.map(item => { - const [, name] = item.name.match(/epg-tapgo-([^.]+).json/) - return { - site_id: item.id, - name - } - }) - } -} - -function parseStart(item) { - return dayjs(item.startTime) -} - -function parseStop(item) { - return dayjs(item.endTime) -} - -function parseItems(content, date) { - if (!content) return [] - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - const d = date.format('YYYY-MM-DD') - - return data.filter(i => i.startTime.includes(d)) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tapdmv.com', + days: 2, + url({ channel, date }) { + return `https://epg.tapdmv.com/calendar/${ + channel.site_id + }?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=${date.toJSON()}&end=${date + .add(1, 'd') + .toJSON()}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content, date) + items.forEach(item => { + programs.push({ + title: item.program.trim(), + description: item.description, + category: item.genre, + icon: item.thumbnailImage, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://epg.tapdmv.com/calendar?$limit=10000&$sort[createdAt]=-1') + .then(r => r.data.data) + .catch(console.log) + + return items.map(item => { + const [, name] = item.name.match(/epg-tapgo-([^.]+).json/) + return { + site_id: item.id, + name + } + }) + } +} + +function parseStart(item) { + return dayjs(item.startTime) +} + +function parseStop(item) { + return dayjs(item.endTime) +} + +function parseItems(content, date) { + if (!content) return [] + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + const d = date.format('YYYY-MM-DD') + + return data.filter(i => i.startTime.includes(d)) +} diff --git a/sites/tapdmv.com/tapdmv.com.test.js b/sites/tapdmv.com/tapdmv.com.test.js index f5602d96..27f05d64 100644 --- a/sites/tapdmv.com/tapdmv.com.test.js +++ b/sites/tapdmv.com/tapdmv.com.test.js @@ -1,51 +1,51 @@ -// npm run grab -- --site=tapdmv.com -// npm run channels:parse -- --config=./sites/tapdmv.com/tapdmv.com.config.js --output=./sites/tapdmv.com/tapdmv.com.channels.xml - -const { parser, url } = require('./tapdmv.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756', - xmltv_id: 'TAPActionFlix.ph' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z' - ) -}) - -it('can parse response', () => { - const content = - '[{"id":"0afc3cc0-eab8-4960-a8b5-55d76edeb8f0","program":"The Bourne Ultimatum","episode":"The Bourne Ultimatum","description":"Jason Bourne dodges a ruthless C.I.A. official and his Agents from a new assassination program while searching for the origins of his life as a trained killer.","genre":"Action","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-03T23:05:00.000Z","endTime":"2022-10-04T01:00:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:10.586Z","updatedAt":"2022-09-30T13:02:10.586Z"},{"id":"8dccd5e0-ab88-44b6-a2af-18d31c6e9ed7","program":"The Devil Inside ","episode":"The Devil Inside ","description":"In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.","genre":"Horror","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-04T01:00:00.000Z","endTime":"2022-10-04T02:25:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:24.031Z","updatedAt":"2022-09-30T13:02:24.031Z"}]' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-04T01:00:00.000Z', - stop: '2022-10-04T02:25:00.000Z', - title: 'The Devil Inside', - description: - 'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.', - category: 'Horror', - icon: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]', - date - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tapdmv.com +// npm run channels:parse -- --config=./sites/tapdmv.com/tapdmv.com.config.js --output=./sites/tapdmv.com/tapdmv.com.channels.xml + +const { parser, url } = require('./tapdmv.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '94b7db9b-5bbd-47d3-a2d3-ce792342a756', + xmltv_id: 'TAPActionFlix.ph' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg.tapdmv.com/calendar/94b7db9b-5bbd-47d3-a2d3-ce792342a756?%24limit=10000&%24sort%5BcreatedAt%5D=-1&start=2022-10-04T00:00:00.000Z&end=2022-10-05T00:00:00.000Z' + ) +}) + +it('can parse response', () => { + const content = + '[{"id":"0afc3cc0-eab8-4960-a8b5-55d76edeb8f0","program":"The Bourne Ultimatum","episode":"The Bourne Ultimatum","description":"Jason Bourne dodges a ruthless C.I.A. official and his Agents from a new assassination program while searching for the origins of his life as a trained killer.","genre":"Action","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-03T23:05:00.000Z","endTime":"2022-10-04T01:00:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:10.586Z","updatedAt":"2022-09-30T13:02:10.586Z"},{"id":"8dccd5e0-ab88-44b6-a2af-18d31c6e9ed7","program":"The Devil Inside ","episode":"The Devil Inside ","description":"In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.","genre":"Horror","thumbnailImage":"https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png","startTime":"2022-10-04T01:00:00.000Z","endTime":"2022-10-04T02:25:00.000Z","fileId":"94b7db9b-5bbd-47d3-a2d3-ce792342a756","createdAt":"2022-09-30T13:02:24.031Z","updatedAt":"2022-09-30T13:02:24.031Z"}]' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-04T01:00:00.000Z', + stop: '2022-10-04T02:25:00.000Z', + title: 'The Devil Inside', + description: + 'In Italy, a woman becomes involved in a series of unauthorized exorcisms during her mission to discover what happened to her mother, who allegedly murdered three people during her own exorcism.', + category: 'Horror', + icon: 'https://s3.ap-southeast-1.amazonaws.com/epg.tapdmv.com/tapactionflix.png' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telecablesat.fr/telecablesat.fr.config.js b/sites/telecablesat.fr/telecablesat.fr.config.js index 6a81cb72..f3ef396e 100644 --- a/sites/telecablesat.fr/telecablesat.fr.config.js +++ b/sites/telecablesat.fr/telecablesat.fr.config.js @@ -1,107 +1,107 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -const API_ENDPOINT = 'https://tv-programme.telecablesat.fr/chaine' - -module.exports = { - site: 'telecablesat.fr', - days: 2, - delay: 5000, - url: function ({ channel, date }) { - return `${API_ENDPOINT}/${channel.site_id}/index.html?date=${date.format('YYYY-MM-DD')}` - }, - async parser({ content, date, channel }) { - let programs = [] - let items = parseItems(content) - if (!items.length) return programs - const url = `${API_ENDPOINT}/${channel.site_id}/index.html` - const promises = [ - axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=noon`), - axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=afternoon`) - ] - await Promise.allSettled(promises).then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data)) - } - }) - }) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://tv-programme.telecablesat.fr/') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $( - '#ptgv_left > section.main > div > div > div:nth-child(1) > div > div > div.linker.with_search > div.inside > div.scroller > a' - ).toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const link = $item('*').attr('href') - const [, site_id] = link.match(/\/chaine\/(\d+)\//) || [null, null] - const name = $item('*').text().trim() - return { - lang: 'fr', - site_id, - name - } - }) - } -} - -function parseStart($item, date) { - const timeString = $item('.schedule-hour').text() - if (!timeString) return null - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${timeString}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Paris' - }).toUTC() -} - -function parseIcon($item) { - const imgSrc = $item('img').attr('src') - - return imgSrc ? `https:${imgSrc}` : null -} - -function parseTitle($item) { - return $item('div.item-content > div.title-left').text().trim() -} - -function parseDescription($item) { - return $item('div.item-content > p').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - '#ptgv_left > div.container > div.row.no-gutter > div.col-md-8 > div > div > div > div > div > div > div.news' - ).toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +const API_ENDPOINT = 'https://tv-programme.telecablesat.fr/chaine' + +module.exports = { + site: 'telecablesat.fr', + days: 2, + delay: 5000, + url: function ({ channel, date }) { + return `${API_ENDPOINT}/${channel.site_id}/index.html?date=${date.format('YYYY-MM-DD')}` + }, + async parser({ content, date, channel }) { + let programs = [] + let items = parseItems(content) + if (!items.length) return programs + const url = `${API_ENDPOINT}/${channel.site_id}/index.html` + const promises = [ + axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=noon`), + axios.get(`${url}?date=${date.format('YYYY-MM-DD')}&period=afternoon`) + ] + await Promise.allSettled(promises).then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data)) + } + }) + }) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://tv-programme.telecablesat.fr/') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $( + '#ptgv_left > section.main > div > div > div:nth-child(1) > div > div > div.linker.with_search > div.inside > div.scroller > a' + ).toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const link = $item('*').attr('href') + const [, site_id] = link.match(/\/chaine\/(\d+)\//) || [null, null] + const name = $item('*').text().trim() + return { + lang: 'fr', + site_id, + name + } + }) + } +} + +function parseStart($item, date) { + const timeString = $item('.schedule-hour').text() + if (!timeString) return null + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${timeString}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Paris' + }).toUTC() +} + +function parseIcon($item) { + const imgSrc = $item('img').attr('src') + + return imgSrc ? `https:${imgSrc}` : null +} + +function parseTitle($item) { + return $item('div.item-content > div.title-left').text().trim() +} + +function parseDescription($item) { + return $item('div.item-content > p').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + '#ptgv_left > div.container > div.row.no-gutter > div.col-md-8 > div > div > div > div > div > div > div.news' + ).toArray() +} diff --git a/sites/telecablesat.fr/telecablesat.fr.test.js b/sites/telecablesat.fr/telecablesat.fr.test.js index b6ec40a8..47f1db5f 100644 --- a/sites/telecablesat.fr/telecablesat.fr.test.js +++ b/sites/telecablesat.fr/telecablesat.fr.test.js @@ -1,116 +1,116 @@ -// npm run channels:parse -- --config=./sites/telecablesat.fr/telecablesat.fr.config.js --output=./sites/telecablesat.fr/telecablesat.fr.channels.xml -// npm run grab -- --site=telecablesat.fr - -const { parser, url } = require('./telecablesat.fr.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: '13emeRue.fr' -} - -jest.mock('axios') - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11' - ) -}) - -it('can parse response', done => { - const content = - '
    06:25

    Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...

    08:05

    Alors que les fêtes de fin d\'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d\'être retrouvée assassinée. Le vieux limier ne croit pas à l\'hypothèse...

    ' - - axios.get.mockImplementation(url => { - if ( - url === 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=noon' - ) { - return Promise.resolve({ - data: '
    12:35

    Brett, Foster et Kidd font une virée dans l\'Indiana. Sur la route, ils sont les témoins d\'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...

    ' - }) - } else if ( - url === - 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=afternoon' - ) { - return Promise.resolve({ - data: '
    01:45

    Lors d\'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu\'ils devront se préparer à une évaluation de leur performance. Otis apprend qu\'il a remporté un prix...

    05:05

    Nos programmes se terminent pour cette journée, en attendant ceux de demain.

    ' - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, date, channel }) - .then(result => { - result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-11T05:25:00.000Z', - stop: '2022-03-11T07:05:00.000Z', - title: 'Alex Hugo', - description: - 'Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...', - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2pQvC0r6FCyxLq6aUFEyMxr_rvGA-IobGYhFb77UtXnq516FuRz_aQKmM4f08mdPKY-HuJR2lh1vh02RHABRSnukK0-wYMwBERBei1rOXUwBjOc2oCSgbIqUQ0KrILqNh5VwEl9iO97qKi9hDe_wf1uJLOyO-Pw.jpg' - }, - { - start: '2022-03-11T07:05:00.000Z', - stop: '2022-03-11T11:35:00.000Z', - title: 'Les saisons meurtrières : hiver rouge', - description: - "Alors que les fêtes de fin d'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d'être retrouvée assassinée. Le vieux limier ne croit pas à l'hypothèse...", - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEEDRu7Cu4AgIeBY2DAzaNiIRmjRteve6_Mlb_C_D0IgtbOu9tsULL_Y61O3oRxtiKjx8XidRWokX6l7Eo_RwL3TqbCNADCm7qGB0lkwGADTGBpTSeXEBbeyk1EguAybptJbyapyQAmogG9VMwB91ZTe2h_a8PubZvKXV7PcH.jpg' - }, - { - start: '2022-03-11T11:35:00.000Z', - stop: '2022-03-12T00:45:00.000Z', - title: 'Chicago Fire', - description: - "Brett, Foster et Kidd font une virée dans l'Indiana. Sur la route, ils sont les témoins d'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...", - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3DWwpayQN_CZaFLq6aUFEyMxr_rvGA-IlJjMYut99rmIIPc67VuRz_aNS1loPfzZF5WHgr3INNROt0Knya7BJBoyT5NoLxjmwEgWusoau2DtCqBc1OOiCZNI2vvVLSAigzl0WSvAVFHPdzrKi5ip_b4PxDtSzsjvj8.jpg' - }, - { - start: '2022-03-12T00:45:00.000Z', - stop: '2022-03-12T04:05:00.000Z', - title: 'Chicago Fire', - description: - "Lors d'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu'ils devront se préparer à une évaluation de leur performance. Otis apprend qu'il a remporté un prix...", - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2hZVSoG_hsixLq6aUFEyMxr_rvGA-ImJjsYit99qWIIPc61C3ox9toFRGfD9P5rTyWLgHSUfpeCt8muwIgDBlTxMo79hmAIjWOoxa-yCT9loxMqIBjhMb7eFqk8qTy6CJZ01GUTTjva7iInZsj_9jnu1LOyO-Pw.jpg' - }, - { - start: '2022-03-12T04:05:00.000Z', - stop: '2022-03-12T05:05:00.000Z', - title: 'Fin des programmes', - description: - 'Nos programmes se terminent pour cette journée, en attendant ceux de demain.', - icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5LDsIgEADQu7C2hXEKTHsWNsOvVVNKCiZG4931neB9hOeWxCK23mtbnHRyr0Pdjn60IcQy8vt5phTXNJbUnQxH6Xwr6dSZAkDgmOcwgZop2QwA3lpijzg7yUZ7iHMgUhRMRONz1IioMKvAekpwhYkJx3tdxUXs3B7_hzH2haTF9wc.jpg' - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: - ' ', - date, - channel - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/telecablesat.fr/telecablesat.fr.config.js --output=./sites/telecablesat.fr/telecablesat.fr.channels.xml +// npm run grab -- --site=telecablesat.fr + +const { parser, url } = require('./telecablesat.fr.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: '13emeRue.fr' +} + +jest.mock('axios') + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11' + ) +}) + +it('can parse response', done => { + const content = + '
    06:25

    Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...

    08:05

    Alors que les fêtes de fin d\'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d\'être retrouvée assassinée. Le vieux limier ne croit pas à l\'hypothèse...

    ' + + axios.get.mockImplementation(url => { + if ( + url === 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=noon' + ) { + return Promise.resolve({ + data: '
    12:35

    Brett, Foster et Kidd font une virée dans l\'Indiana. Sur la route, ils sont les témoins d\'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...

    ' + }) + } else if ( + url === + 'https://tv-programme.telecablesat.fr/chaine/2/index.html?date=2022-03-11&period=afternoon' + ) { + return Promise.resolve({ + data: '
    01:45

    Lors d\'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu\'ils devront se préparer à une évaluation de leur performance. Otis apprend qu\'il a remporté un prix...

    05:05

    Nos programmes se terminent pour cette journée, en attendant ceux de demain.

    ' + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, date, channel }) + .then(result => { + result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-11T05:25:00.000Z', + stop: '2022-03-11T07:05:00.000Z', + title: 'Alex Hugo', + description: + 'Des randonneurs font une macabre découverte en installant leur tente dans la nature : une jeune fille de Lusagne a en effet été sauvagement assassinée et son corps a été dissimulé sommairement dans les buissons. Rapidement, des lettres anonymes...', + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2pQvC0r6FCyxLq6aUFEyMxr_rvGA-IobGYhFb77UtXnq516FuRz_aQKmM4f08mdPKY-HuJR2lh1vh02RHABRSnukK0-wYMwBERBei1rOXUwBjOc2oCSgbIqUQ0KrILqNh5VwEl9iO97qKi9hDe_wf1uJLOyO-Pw.jpg' + }, + { + start: '2022-03-11T07:05:00.000Z', + stop: '2022-03-11T11:35:00.000Z', + title: 'Les saisons meurtrières : hiver rouge', + description: + "Alors que les fêtes de fin d'année battent leur plein, le commissaire Rousseau se voit confronté à une délicate affaire. En peu de temps, une troisième jeune fille vient d'être retrouvée assassinée. Le vieux limier ne croit pas à l'hypothèse...", + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDoMgEEDRu7Cu4AgIeBY2DAzaNiIRmjRteve6_Mlb_C_D0IgtbOu9tsULL_Y61O3oRxtiKjx8XidRWokX6l7Eo_RwL3TqbCNADCm7qGB0lkwGADTGBpTSeXEBbeyk1EguAybptJbyapyQAmogG9VMwB91ZTe2h_a8PubZvKXV7PcH.jpg' + }, + { + start: '2022-03-11T11:35:00.000Z', + stop: '2022-03-12T00:45:00.000Z', + title: 'Chicago Fire', + description: + "Brett, Foster et Kidd font une virée dans l'Indiana. Sur la route, ils sont les témoins d'un accident. Un bus qui transporte une équipe de hockey percute une voiture en panne. Sans réseau téléphonique, ils ne peuvent prévenir leurs collègues. En...", + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3DWwpayQN_CZaFLq6aUFEyMxr_rvGA-IlJjMYut99rmIIPc67VuRz_aNS1loPfzZF5WHgr3INNROt0Knya7BJBoyT5NoLxjmwEgWusoau2DtCqBc1OOiCZNI2vvVLSAigzl0WSvAVFHPdzrKi5ip_b4PxDtSzsjvj8.jpg' + }, + { + start: '2022-03-12T00:45:00.000Z', + stop: '2022-03-12T04:05:00.000Z', + title: 'Chicago Fire', + description: + "Lors d'une urgence, la vie du lieutenant Casey est soudainement mise en danger : un homme le menace avec une arme. Grissom prévient la caserne qu'ils devront se préparer à une évaluation de leur performance. Otis apprend qu'il a remporté un prix...", + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5BDsIgEADAv3C2hZVSoG_hsixLq6aUFEyMxr_rvGA-ImJjsYit99qWIIPc61C3ox9toFRGfD9P5rTyWLgHSUfpeCt8muwIgDBlTxMo79hmAIjWOoxa-yCT9loxMqIBjhMb7eFqk8qTy6CJZ01GUTTjva7iInZsj_9jnu1LOyO-Pw.jpg' + }, + { + start: '2022-03-12T04:05:00.000Z', + stop: '2022-03-12T05:05:00.000Z', + title: 'Fin des programmes', + description: + 'Nos programmes se terminent pour cette journée, en attendant ceux de demain.', + icon: 'https://tv.cdnartwhere.eu/cache/i2/Dc5LDsIgEADQu7C2hXEKTHsWNsOvVVNKCiZG4931neB9hOeWxCK23mtbnHRyr0Pdjn60IcQy8vt5phTXNJbUnQxH6Xwr6dSZAkDgmOcwgZop2QwA3lpijzg7yUZ7iHMgUhRMRONz1IioMKvAekpwhYkJx3tdxUXs3B7_hzH2haTF9wc.jpg' + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: + ' ', + date, + channel + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/telenet.tv/telenet.tv.config.js b/sites/telenet.tv/telenet.tv.config.js index e9dde5d3..38d5a57f 100644 --- a/sites/telenet.tv/telenet.tv.config.js +++ b/sites/telenet.tv/telenet.tv.config.js @@ -1,132 +1,132 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' -const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' - -module.exports = { - site: 'telenet.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const promises = [ - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(6, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(12, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date - .add(18, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ) - ] - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item, channel) - programs.push({ - title: item.title, - description: detail.longDescription, - category: detail.genres, - actors: detail.actors, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=en&productClass=Orion-DASH`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'be', - site_id: item.id, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item, channel) { - if (!item.id) return {} - const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.entries)) return [] - const channelData = data.entries.find(e => e.channelId === channel.site_id) - if (!channelData) return [] - - return Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber -} - -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' +const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' + +module.exports = { + site: 'telenet.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + return `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(6, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(12, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_STATIC_ENDPOINT}/${channel.lang}/events/segments/${date + .add(18, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item, channel) + programs.push({ + title: item.title, + description: detail.longDescription, + category: detail.genres, + actors: detail.actors, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get(`${API_PROD_ENDPOINT}/channels?cityId=65535&language=en&productClass=Orion-DASH`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'be', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item, channel) { + if (!item.id) return {} + const url = `${API_PROD_ENDPOINT}/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} diff --git a/sites/telenet.tv/telenet.tv.test.js b/sites/telenet.tv/telenet.tv.test.js index 4202e27d..ccaede56 100644 --- a/sites/telenet.tv/telenet.tv.test.js +++ b/sites/telenet.tv/telenet.tv.test.js @@ -1,91 +1,91 @@ -// npm run channels:parse -- --config=./sites/telenet.tv/telenet.tv.config.js --output=./sites/telenet.tv/telenet.tv.channels.xml -// npm run grab -- --site=telenet.tv - -const { parser, url } = require('./telenet.tv.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' -const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'outtv', - xmltv_id: 'OutTV.nl', - lang: 'nl' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe(`${API_STATIC_ENDPOINT}/nl/events/segments/20221030000000`) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030060000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030120000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030180000`) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106?returnLinearContent=true&language=nl` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T23:56:00.000Z', - stop: '2022-10-30T01:44:00.000Z', - title: 'Queer as Folk USA', - description: - "Justin belandt in de gevangenis, Brian en Brandon banen zich een weg door de lijst, Ben treurt, Melanie en Lindsay proberen een interne scheiding en Emmett's stalker onthult zichzelf.", - category: ['Dramaserie', 'LHBTI'], - actors: [ - 'Gale Harold', - 'Hal Sparks', - 'Randy Harrison', - 'Peter Paige', - 'Scott Lowell', - 'Thea Gill', - 'Michelle Clunie', - 'Sharon Gless' - ], - season: 5, - episode: 8 - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/telenet.tv/telenet.tv.config.js --output=./sites/telenet.tv/telenet.tv.channels.xml +// npm run grab -- --site=telenet.tv + +const { parser, url } = require('./telenet.tv.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' +const API_PROD_ENDPOINT = 'https://prod.spark.telenet.tv/eng/web/linear-service/v2' + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'outtv', + xmltv_id: 'OutTV.nl', + lang: 'nl' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe(`${API_STATIC_ENDPOINT}/nl/events/segments/20221030000000`) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030060000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030120000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if (url === `${API_STATIC_ENDPOINT}/nl/events/segments/20221030180000`) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + `${API_PROD_ENDPOINT}/replayEvent/crid:~~2F~~2Fgn.tv~~2F2459095~~2FEP036477800004,imi:0a2f4207b03c16c70b7fb3be8e07881aafe44106?returnLinearContent=true&language=nl` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T23:56:00.000Z', + stop: '2022-10-30T01:44:00.000Z', + title: 'Queer as Folk USA', + description: + "Justin belandt in de gevangenis, Brian en Brandon banen zich een weg door de lijst, Ben treurt, Melanie en Lindsay proberen een interne scheiding en Emmett's stalker onthult zichzelf.", + category: ['Dramaserie', 'LHBTI'], + actors: [ + 'Gale Harold', + 'Hal Sparks', + 'Randy Harrison', + 'Peter Paige', + 'Scott Lowell', + 'Thea Gill', + 'Michelle Clunie', + 'Sharon Gless' + ], + season: 5, + episode: 8 + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/teliatv.ee/teliatv.ee.config.js b/sites/teliatv.ee/teliatv.ee.config.js index 56406661..426b7fd3 100644 --- a/sites/teliatv.ee/teliatv.ee.config.js +++ b/sites/teliatv.ee/teliatv.ee.config.js @@ -1,67 +1,67 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'teliatv.ee', - days: 2, - url({ date, channel }) { - const [lang, channelId] = channel.site_id.split('#') - return `https://api.teliatv.ee/dtv-api/3.2/${lang}/epg/guide?channelIds=${channelId}&relations=programmes&images=webGuideItemLarge&startAt=${date - .add(1, 'd') - .format('YYYY-MM-DDTHH:mm')}&startAtOp=lte&endAt=${date.format( - 'YYYY-MM-DDTHH:mm' - )}&endAtOp=gt` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.name, - icon: parseIcon(item), - start: dayjs(item.startAt), - stop: dayjs(item.endAt) - }) - }) - - return programs - }, - async channels({ lang }) { - const data = await axios - .get(`https://api.teliatv.ee/dtv-api/3.0/${lang}/channel-lists?listClass=tv&ui=tv-web`) - .then(r => r.data) - .catch(console.log) - - return Object.values(data.channels).map(item => { - return { - lang, - site_id: item.id, - name: item.title - } - }) - } -} - -function parseIcon(item) { - return item.images.webGuideItemLarge - ? `https://inet-static.mw.elion.ee${item.images.webGuideItemLarge}` - : null -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !data.relations || !data.categoryItems) return [] - const [, channelId] = channel.site_id.split('#') - const items = data.categoryItems[channelId] || [] - - return items - .map(i => { - const programmeId = i.related.programmeIds[0] - if (!programmeId) return null - const progData = data.relations.programmes[programmeId] - if (!progData) return null - - return { ...i, ...progData } - }) - .filter(i => i) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'teliatv.ee', + days: 2, + url({ date, channel }) { + const [lang, channelId] = channel.site_id.split('#') + return `https://api.teliatv.ee/dtv-api/3.2/${lang}/epg/guide?channelIds=${channelId}&relations=programmes&images=webGuideItemLarge&startAt=${date + .add(1, 'd') + .format('YYYY-MM-DDTHH:mm')}&startAtOp=lte&endAt=${date.format( + 'YYYY-MM-DDTHH:mm' + )}&endAtOp=gt` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.name, + icon: parseIcon(item), + start: dayjs(item.startAt), + stop: dayjs(item.endAt) + }) + }) + + return programs + }, + async channels({ lang }) { + const data = await axios + .get(`https://api.teliatv.ee/dtv-api/3.0/${lang}/channel-lists?listClass=tv&ui=tv-web`) + .then(r => r.data) + .catch(console.log) + + return Object.values(data.channels).map(item => { + return { + lang, + site_id: item.id, + name: item.title + } + }) + } +} + +function parseIcon(item) { + return item.images.webGuideItemLarge + ? `https://inet-static.mw.elion.ee${item.images.webGuideItemLarge}` + : null +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !data.relations || !data.categoryItems) return [] + const [, channelId] = channel.site_id.split('#') + const items = data.categoryItems[channelId] || [] + + return items + .map(i => { + const programmeId = i.related.programmeIds[0] + if (!programmeId) return null + const progData = data.relations.programmes[programmeId] + if (!progData) return null + + return { ...i, ...progData } + }) + .filter(i => i) +} diff --git a/sites/teliatv.ee/teliatv.ee.test.js b/sites/teliatv.ee/teliatv.ee.test.js index 4c6ef11b..a41622ef 100644 --- a/sites/teliatv.ee/teliatv.ee.test.js +++ b/sites/teliatv.ee/teliatv.ee.test.js @@ -1,61 +1,61 @@ -// npm run channels:parse -- --config=./sites/teliatv.ee/teliatv.ee.config.js --output=./sites/teliatv.ee/teliatv.ee.channels.xml --set=lang:et -// npm run grab -- --site=teliatv.ee - -const { parser, url } = require('./teliatv.ee.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-20', 'YYYY-MM-DD').startOf('d') -const channel = { - lang: 'et', - site_id: 'et#1', - xmltv_id: 'ETV.ee' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' - ) -}) - -it('can generate valid url with different language', () => { - const ruChannel = { - lang: 'ru', - site_id: 'ru#1', - xmltv_id: 'ETV.ee' - } - expect(url({ date, channel: ruChannel })).toBe( - 'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' - ) -}) - -it('can parse response', () => { - const content = - '{"categoryItems":{"1":[{"id":136227,"type":"epgSeries","name":"Inimjaht","originalName":"Manhunt","price":null,"owner":"ETV","ownerId":1,"images":{"webGuideItemLarge":"/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg"},"packetIds":[30,34,38,129,130,162,191,242,243,244,447,483,484,485,486],"related":{"programmeIds":[27224371]}}]},"relations":{"programmes":{"27224371":{"id":27224371,"startAt":"2021-11-20T00:05:00+02:00","endAt":"2021-11-20T00:55:00+02:00","publicTo":"2021-12-04T02:05:00+02:00","status":"default","channelId":1,"broadcastId":78248901,"hasMarkers":false,"catchup":false}}}}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-19T22:05:00.000Z', - stop: '2021-11-19T22:55:00.000Z', - title: 'Inimjaht', - icon: 'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"categoryItems":{},"relations":{}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/teliatv.ee/teliatv.ee.config.js --output=./sites/teliatv.ee/teliatv.ee.channels.xml --set=lang:et +// npm run grab -- --site=teliatv.ee + +const { parser, url } = require('./teliatv.ee.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-20', 'YYYY-MM-DD').startOf('d') +const channel = { + lang: 'et', + site_id: 'et#1', + xmltv_id: 'ETV.ee' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://api.teliatv.ee/dtv-api/3.2/et/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' + ) +}) + +it('can generate valid url with different language', () => { + const ruChannel = { + lang: 'ru', + site_id: 'ru#1', + xmltv_id: 'ETV.ee' + } + expect(url({ date, channel: ruChannel })).toBe( + 'https://api.teliatv.ee/dtv-api/3.2/ru/epg/guide?channelIds=1&relations=programmes&images=webGuideItemLarge&startAt=2021-11-21T00:00&startAtOp=lte&endAt=2021-11-20T00:00&endAtOp=gt' + ) +}) + +it('can parse response', () => { + const content = + '{"categoryItems":{"1":[{"id":136227,"type":"epgSeries","name":"Inimjaht","originalName":"Manhunt","price":null,"owner":"ETV","ownerId":1,"images":{"webGuideItemLarge":"/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg"},"packetIds":[30,34,38,129,130,162,191,242,243,244,447,483,484,485,486],"related":{"programmeIds":[27224371]}}]},"relations":{"programmes":{"27224371":{"id":27224371,"startAt":"2021-11-20T00:05:00+02:00","endAt":"2021-11-20T00:55:00+02:00","publicTo":"2021-12-04T02:05:00+02:00","status":"default","channelId":1,"broadcastId":78248901,"hasMarkers":false,"catchup":false}}}}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-19T22:05:00.000Z', + stop: '2021-11-19T22:55:00.000Z', + title: 'Inimjaht', + icon: 'https://inet-static.mw.elion.ee/resized/ri93Qj4OLXXvg7QAsUOcKMnIb3g=/570x330/filters:format(jpeg)/inet-static.mw.elion.ee/epg_images/9/b/17e48b3966e65c02.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"categoryItems":{},"relations":{}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telkku.com/telkku.com.config.js b/sites/telkku.com/telkku.com.config.js index bd7fb8ae..3ce3ac6b 100644 --- a/sites/telkku.com/telkku.com.config.js +++ b/sites/telkku.com/telkku.com.config.js @@ -1,72 +1,72 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'telkku.com', - days: 2, - url: function ({ channel, date }) { - const [group] = channel.site_id.split('#') - - return `https://telkku.com/api/channel-groups/${group}/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=${date.format( - 'YYYY-MM-DD' - )}&view=PublicationDetails` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = getItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - icon: getIcon(item), - start: getStart(item), - stop: getStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://telkku.com/api/channel-groups') - .then(r => r.data) - .catch(console.log) - - let items = [] - data.response.forEach(group => { - group.channels.forEach(channel => { - items.push({ - lang: 'fi', - site_id: `${group.id}#${channel.id}`, - name: channel.name - }) - }) - }) - - return items - } -} - -function getIcon(item) { - const image = item.images.find(i => i.type === 'default' && i.sizeTag === '1200x630') - - return image ? image.url : null -} - -function getStart(item) { - return dayjs(item.startTime) -} - -function getStop(item) { - return dayjs(item.endTime) -} - -function getItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !data.response || !Array.isArray(data.response.publicationsByChannel)) return [] - const channelData = data.response.publicationsByChannel.find(i => i.channel.id === channelId) - if (!channelData || !Array.isArray(channelData.publications)) return [] - - return channelData.publications -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'telkku.com', + days: 2, + url: function ({ channel, date }) { + const [group] = channel.site_id.split('#') + + return `https://telkku.com/api/channel-groups/${group}/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=${date.format( + 'YYYY-MM-DD' + )}&view=PublicationDetails` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = getItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + icon: getIcon(item), + start: getStart(item), + stop: getStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://telkku.com/api/channel-groups') + .then(r => r.data) + .catch(console.log) + + let items = [] + data.response.forEach(group => { + group.channels.forEach(channel => { + items.push({ + lang: 'fi', + site_id: `${group.id}#${channel.id}`, + name: channel.name + }) + }) + }) + + return items + } +} + +function getIcon(item) { + const image = item.images.find(i => i.type === 'default' && i.sizeTag === '1200x630') + + return image ? image.url : null +} + +function getStart(item) { + return dayjs(item.startTime) +} + +function getStop(item) { + return dayjs(item.endTime) +} + +function getItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !data.response || !Array.isArray(data.response.publicationsByChannel)) return [] + const channelData = data.response.publicationsByChannel.find(i => i.channel.id === channelId) + if (!channelData || !Array.isArray(channelData.publications)) return [] + + return channelData.publications +} diff --git a/sites/telkku.com/telkku.com.test.js b/sites/telkku.com/telkku.com.test.js index 2946fdc0..d03dca75 100644 --- a/sites/telkku.com/telkku.com.test.js +++ b/sites/telkku.com/telkku.com.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/telkku.com/telkku.com.config.js --output=./sites/telkku.com/telkku.com.channels.xml -// npm run grab -- --site=telkku.com - -const { parser, url } = require('./telkku.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'default_builtin_channelgroup1#yle-tv1', - xmltv_id: 'YleTV1.fi' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://telkku.com/api/channel-groups/default_builtin_channelgroup1/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=2022-10-29&view=PublicationDetails' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-28T20:50:00.000Z', - stop: '2022-10-28T21:20:00.000Z', - title: 'Puoli seitsemän', - description: - 'Vieraana näyttelijä Elias Salonen. Puoli seiskassa vietetään sekä halloweeniä että joulua, kun Olli-Pekka tapaa todellisen jouluttajan. Juontajina Anniina Valtonen, Tuulianna Tola ja Olli-Pekka Kursi.', - icon: 'https://thumbor.prod.telkku.com/YTglotoUl7aJtzPtYnvM9tH03sY=/1200x630/smart/filters:quality(86):format(jpeg)/img.prod.telkku.com/program-images/0f885238ac16ce167a9d80eace450254.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')), - channel - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/telkku.com/telkku.com.config.js --output=./sites/telkku.com/telkku.com.channels.xml +// npm run grab -- --site=telkku.com + +const { parser, url } = require('./telkku.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'default_builtin_channelgroup1#yle-tv1', + xmltv_id: 'YleTV1.fi' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://telkku.com/api/channel-groups/default_builtin_channelgroup1/offering?startTime=00%3A00%3A00.000&duration=PT24H&inclusionPolicy=IncludeOngoingAlso&limit=1000&tvDate=2022-10-29&view=PublicationDetails' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-28T20:50:00.000Z', + stop: '2022-10-28T21:20:00.000Z', + title: 'Puoli seitsemän', + description: + 'Vieraana näyttelijä Elias Salonen. Puoli seiskassa vietetään sekä halloweeniä että joulua, kun Olli-Pekka tapaa todellisen jouluttajan. Juontajina Anniina Valtonen, Tuulianna Tola ja Olli-Pekka Kursi.', + icon: 'https://thumbor.prod.telkku.com/YTglotoUl7aJtzPtYnvM9tH03sY=/1200x630/smart/filters:quality(86):format(jpeg)/img.prod.telkku.com/program-images/0f885238ac16ce167a9d80eace450254.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')), + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/telkussa.fi/telkussa.fi.config.js b/sites/telkussa.fi/telkussa.fi.config.js index a887f4ca..d059f273 100644 --- a/sites/telkussa.fi/telkussa.fi.config.js +++ b/sites/telkussa.fi/telkussa.fi.config.js @@ -1,30 +1,30 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'telkussa.fi', - days: 2, - url: function ({ date, channel }) { - return `https://telkussa.fi/API/Channel/${channel.site_id}/${date.format('YYYYMMDD')}` - }, - parser: function ({ content }) { - const programs = [] - const items = JSON.parse(content) - if (!items.length) return programs - - items.forEach(item => { - if (item.name && item.start && item.stop) { - const start = dayjs.unix(parseInt(item.start) * 60) - const stop = dayjs.unix(parseInt(item.stop) * 60) - - programs.push({ - title: item.name, - description: item.description, - start: start.toString(), - stop: stop.toString() - }) - } - }) - - return programs - } -} +const dayjs = require('dayjs') + +module.exports = { + site: 'telkussa.fi', + days: 2, + url: function ({ date, channel }) { + return `https://telkussa.fi/API/Channel/${channel.site_id}/${date.format('YYYYMMDD')}` + }, + parser: function ({ content }) { + const programs = [] + const items = JSON.parse(content) + if (!items.length) return programs + + items.forEach(item => { + if (item.name && item.start && item.stop) { + const start = dayjs.unix(parseInt(item.start) * 60) + const stop = dayjs.unix(parseInt(item.stop) * 60) + + programs.push({ + title: item.name, + description: item.description, + start: start.toString(), + stop: stop.toString() + }) + } + }) + + return programs + } +} diff --git a/sites/telsu.fi/telsu.fi.config.js b/sites/telsu.fi/telsu.fi.config.js index cee4a1fe..4aae8452 100644 --- a/sites/telsu.fi/telsu.fi.config.js +++ b/sites/telsu.fi/telsu.fi.config.js @@ -1,98 +1,98 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'telsu.fi', - days: 2, - url: function ({ date, channel }) { - return `https://www.telsu.fi/${date.format('YYYYMMDD')}/${channel.site_id}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - let stop = parseStop($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - } - prev.stop = start - if (stop.isBefore(prev.stop)) { - stop = stop.add(1, 'd') - } - } - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const html = await axios - .get('https://www.telsu.fi/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(html) - const items = $('.ch').toArray() - return items.map(item => { - const name = $(item).find('a').attr('title') - const site_id = $(item).attr('rel') - - return { - lang: 'fi', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('h1 > b').text().trim() -} - -function parseDescription($item) { - return $item('.t > div').clone().children().remove().end().text().trim() -} - -function parseIcon($item) { - const imgSrc = $item('.t > div > div.ps > a > img').attr('src') - - return imgSrc ? `https://www.telsu.fi${imgSrc}` : null -} - -function parseStart($item, date) { - const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() - const [, HH, mm] = subtitle.match(/(\d{2})\.(\d{2}) - (\d{2})\.(\d{2})$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') -} - -function parseStop($item, date) { - const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() - const [, HH, mm] = subtitle.match(/ - (\d{2})\.(\d{2})$/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#res > div.dets').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'telsu.fi', + days: 2, + url: function ({ date, channel }) { + return `https://www.telsu.fi/${date.format('YYYYMMDD')}/${channel.site_id}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + let stop = parseStop($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + } + prev.stop = start + if (stop.isBefore(prev.stop)) { + stop = stop.add(1, 'd') + } + } + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const html = await axios + .get('https://www.telsu.fi/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(html) + const items = $('.ch').toArray() + return items.map(item => { + const name = $(item).find('a').attr('title') + const site_id = $(item).attr('rel') + + return { + lang: 'fi', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('h1 > b').text().trim() +} + +function parseDescription($item) { + return $item('.t > div').clone().children().remove().end().text().trim() +} + +function parseIcon($item) { + const imgSrc = $item('.t > div > div.ps > a > img').attr('src') + + return imgSrc ? `https://www.telsu.fi${imgSrc}` : null +} + +function parseStart($item, date) { + const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() + const [, HH, mm] = subtitle.match(/(\d{2})\.(\d{2}) - (\d{2})\.(\d{2})$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') +} + +function parseStop($item, date) { + const subtitle = $item('.h > h2').clone().children().remove().end().text().trim() + const [, HH, mm] = subtitle.match(/ - (\d{2})\.(\d{2})$/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Helsinki') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#res > div.dets').toArray() +} diff --git a/sites/telsu.fi/telsu.fi.test.js b/sites/telsu.fi/telsu.fi.test.js index e2e99577..b3f17a9b 100644 --- a/sites/telsu.fi/telsu.fi.test.js +++ b/sites/telsu.fi/telsu.fi.test.js @@ -1,47 +1,47 @@ -// npm run channels:parse -- --config=./sites/telsu.fi/telsu.fi.config.js --output=./sites/telsu.fi/telsu.fi.channels.xml -// npm run grab -- --site=telsu.fi - -const { parser, url } = require('./telsu.fi.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'yle1', - xmltv_id: 'YleTV1.fi' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://www.telsu.fi/20221029/yle1') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T04:00:00.000Z', - stop: '2022-10-29T04:28:00.000Z', - title: 'Antiikkikaksintaistelu', - description: - 'Kausi 6, osa 5/12. Antiikkikaksintaistelu jatkuu Løkkenissä. Uusi taistelupari Rikke Fog ja Lasse Franck saavat kumpikin 10 000 kruunua ja viisi tuntia aikaa ostaa alueelta hyvää tavaraa halvalla.', - icon: 'https://www.telsu.fi/s/antiikkikaksintaistelu_11713730.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), - date - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/telsu.fi/telsu.fi.config.js --output=./sites/telsu.fi/telsu.fi.channels.xml +// npm run grab -- --site=telsu.fi + +const { parser, url } = require('./telsu.fi.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'yle1', + xmltv_id: 'YleTV1.fi' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://www.telsu.fi/20221029/yle1') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T04:00:00.000Z', + stop: '2022-10-29T04:28:00.000Z', + title: 'Antiikkikaksintaistelu', + description: + 'Kausi 6, osa 5/12. Antiikkikaksintaistelu jatkuu Løkkenissä. Uusi taistelupari Rikke Fog ja Lasse Franck saavat kumpikin 10 000 kruunua ja viisi tuntia aikaa ostaa alueelta hyvää tavaraa halvalla.', + icon: 'https://www.telsu.fi/s/antiikkikaksintaistelu_11713730.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')), + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tivu.tv/tivu.tv.config.js b/sites/tivu.tv/tivu.tv.config.js index 5e8dcd5f..8f16d2d4 100644 --- a/sites/tivu.tv/tivu.tv.config.js +++ b/sites/tivu.tv/tivu.tv.config.js @@ -1,64 +1,64 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tivu.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ date }) { - const diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') - - return `https://www.tivu.tv/epg_ajax_sat.aspx?d=${diff}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - const [title] = $item('a').html().split('
    ') - - return title -} - -function parseStart($item, date) { - const [, , time] = $item('a').html().split('
    ') - if (!time) return null - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Rome' - }).toUTC() -} - -function parseItems(content, channel) { - if (!content) return [] - const $ = cheerio.load(content) - - return $(`.q[id="${channel.site_id}"] > .p`).toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tivu.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ date }) { + const diff = date.diff(DateTime.now().toUTC().startOf('day'), 'd') + + return `https://www.tivu.tv/epg_ajax_sat.aspx?d=${diff}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + const [title] = $item('a').html().split('
    ') + + return title +} + +function parseStart($item, date) { + const [, , time] = $item('a').html().split('
    ') + if (!time) return null + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Rome' + }).toUTC() +} + +function parseItems(content, channel) { + if (!content) return [] + const $ = cheerio.load(content) + + return $(`.q[id="${channel.site_id}"] > .p`).toArray() +} diff --git a/sites/tivu.tv/tivu.tv.test.js b/sites/tivu.tv/tivu.tv.test.js index e8521a0d..47ed98e9 100644 --- a/sites/tivu.tv/tivu.tv.test.js +++ b/sites/tivu.tv/tivu.tv.test.js @@ -1,58 +1,58 @@ -// npm run grab -- --site=tivu.tv - -const { parser, url } = require('./tivu.tv.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const channel = { - site_id: '62', - xmltv_id: 'Rai1HD.it' -} - -it('can generate valid url for today', () => { - const date = dayjs.utc().startOf('d') - expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') -}) - -it('can generate valid url for tomorrow', () => { - const date = dayjs.utc().startOf('d').add(1, 'd') - expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=1') -}) - -it('can parse response', () => { - const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-03T22:02:00.000Z', - stop: '2022-10-03T22:45:00.000Z', - title: 'Cose Nostre - La figlia del boss' - }) - - expect(results[43]).toMatchObject({ - start: '2022-10-05T04:58:00.000Z', - stop: '2022-10-05T05:28:00.000Z', - title: 'Tgunomattina - in collaborazione con day' - }) -}) - -it('can handle empty guide', () => { - const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tivu.tv + +const { parser, url } = require('./tivu.tv.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const channel = { + site_id: '62', + xmltv_id: 'Rai1HD.it' +} + +it('can generate valid url for today', () => { + const date = dayjs.utc().startOf('d') + expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=0') +}) + +it('can generate valid url for tomorrow', () => { + const date = dayjs.utc().startOf('d').add(1, 'd') + expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=1') +}) + +it('can parse response', () => { + const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-03T22:02:00.000Z', + stop: '2022-10-03T22:45:00.000Z', + title: 'Cose Nostre - La figlia del boss' + }) + + expect(results[43]).toMatchObject({ + start: '2022-10-05T04:58:00.000Z', + stop: '2022-10-05T05:28:00.000Z', + title: 'Tgunomattina - in collaborazione con day' + }) +}) + +it('can handle empty guide', () => { + const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/toonamiaftermath.com/toonamiaftermath.com.config.js b/sites/toonamiaftermath.com/toonamiaftermath.com.config.js index effd5029..be0cac53 100644 --- a/sites/toonamiaftermath.com/toonamiaftermath.com.config.js +++ b/sites/toonamiaftermath.com/toonamiaftermath.com.config.js @@ -1,60 +1,60 @@ -process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' - -const dayjs = require('dayjs') -const axios = require('axios') - -const API_ENDPOINT = 'https://api.toonamiaftermath.com' - -module.exports = { - site: 'toonamiaftermath.com', - days: 3, - async url({ channel, date }) { - const playlists = await axios - .get( - `${API_ENDPOINT}/playlists?scheduleName=${channel.site_id}&startDate=${date - .add(1, 'd') - .toJSON()}&thisWeek=true&weekStartDay=monday` - ) - .then(r => r.data) - .catch(console.error) - - const playlist = playlists.find(p => date.isSame(p.startDate, 'day')) - - return `${API_ENDPOINT}/playlist?id=${playlist._id}&addInfo=true` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - sub_title: parseEpisode(item), - icon: parseIcon(item), - start: dayjs(item.startDate), - stop: dayjs(item.endDate) - }) - }) - - return programs - } -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !data.playlist) return [] - - return data.playlist.blocks.reduce((acc, curr) => { - acc = acc.concat(curr.mediaList) - - return acc - }, []) -} - -function parseEpisode(item) { - return item && item.info && item.info.episode ? item.info.episode : null -} - -function parseIcon(item) { - return item && item.info && item.info.image ? item.info.image : null -} +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' + +const dayjs = require('dayjs') +const axios = require('axios') + +const API_ENDPOINT = 'https://api.toonamiaftermath.com' + +module.exports = { + site: 'toonamiaftermath.com', + days: 3, + async url({ channel, date }) { + const playlists = await axios + .get( + `${API_ENDPOINT}/playlists?scheduleName=${channel.site_id}&startDate=${date + .add(1, 'd') + .toJSON()}&thisWeek=true&weekStartDay=monday` + ) + .then(r => r.data) + .catch(console.error) + + const playlist = playlists.find(p => date.isSame(p.startDate, 'day')) + + return `${API_ENDPOINT}/playlist?id=${playlist._id}&addInfo=true` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + sub_title: parseEpisode(item), + icon: parseIcon(item), + start: dayjs(item.startDate), + stop: dayjs(item.endDate) + }) + }) + + return programs + } +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !data.playlist) return [] + + return data.playlist.blocks.reduce((acc, curr) => { + acc = acc.concat(curr.mediaList) + + return acc + }, []) +} + +function parseEpisode(item) { + return item && item.info && item.info.episode ? item.info.episode : null +} + +function parseIcon(item) { + return item && item.info && item.info.image ? item.info.image : null +} diff --git a/sites/toonamiaftermath.com/toonamiaftermath.com.test.js b/sites/toonamiaftermath.com/toonamiaftermath.com.test.js index 23418757..47f99ef9 100644 --- a/sites/toonamiaftermath.com/toonamiaftermath.com.test.js +++ b/sites/toonamiaftermath.com/toonamiaftermath.com.test.js @@ -1,63 +1,63 @@ -// npm run grab -- --site=toonamiaftermath.com - -const { parser, url } = require('./toonamiaftermath.com.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const API_ENDPOINT = 'https://api.toonamiaftermath.com' - -const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'Toonami Aftermath EST', - xmltv_id: 'ToonamiAftermathEast.us' -} - -it('can generate valid url', async () => { - axios.get.mockImplementation(url => { - if ( - url === - `${API_ENDPOINT}/playlists?scheduleName=Toonami Aftermath EST&startDate=2022-11-30T00:00:00.000Z&thisWeek=true&weekStartDay=monday` - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/playlists.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - const result = await url({ channel, date }) - - expect(result).toBe(`${API_ENDPOINT}/playlist?id=635fbd8117f6824d953a216e&addInfo=true`) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(62) - expect(results[0]).toMatchObject({ - start: '2022-11-29T17:00:30.231Z', - stop: '2022-11-29T17:20:54.031Z', - title: 'X-Men', - sub_title: 'Reunion (Part 1)', - icon: 'https://i.imgur.com/ZSZ0x1m.gif' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '', date }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=toonamiaftermath.com + +const { parser, url } = require('./toonamiaftermath.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const API_ENDPOINT = 'https://api.toonamiaftermath.com' + +const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'Toonami Aftermath EST', + xmltv_id: 'ToonamiAftermathEast.us' +} + +it('can generate valid url', async () => { + axios.get.mockImplementation(url => { + if ( + url === + `${API_ENDPOINT}/playlists?scheduleName=Toonami Aftermath EST&startDate=2022-11-30T00:00:00.000Z&thisWeek=true&weekStartDay=monday` + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/playlists.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + const result = await url({ channel, date }) + + expect(result).toBe(`${API_ENDPOINT}/playlist?id=635fbd8117f6824d953a216e&addInfo=true`) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(62) + expect(results[0]).toMatchObject({ + start: '2022-11-29T17:00:30.231Z', + stop: '2022-11-29T17:20:54.031Z', + title: 'X-Men', + sub_title: 'Reunion (Part 1)', + icon: 'https://i.imgur.com/ZSZ0x1m.gif' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '', date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/transvision.co.id/transvision.co.id.config.js b/sites/transvision.co.id/transvision.co.id.config.js index 50f9d9ad..ae1ecc21 100644 --- a/sites/transvision.co.id/transvision.co.id.config.js +++ b/sites/transvision.co.id/transvision.co.id.config.js @@ -1,93 +1,93 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'transvision.co.id', - days: 2, - url: 'https://www.transvision.co.id/jadwalacara/epg', - request: { - method: 'POST', - data: function ({ channel, date }) { - const formData = new URLSearchParams() - formData.append('ValidateEPG[channel_name]', channel.site_id) - formData.append('ValidateEPG[tanggal]', date.format('YYYY-MM-DD')) - formData.append('ValidateEPG[sinopsis]', '') - formData.append('yt0', 'PROSES') - - return formData - }, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - for (const item of items) { - const prev = programs[programs.length - 1] - const start = parseStart(item, date) - if (prev) prev.stop = start - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - start, - stop - }) - } - - return programs - }, - async channels() { - const data = await axios - .get('https://www.transvision.co.id/jadwalacara/epg') - .then(response => response.data) - .catch(console.log) - - const $ = cheerio.load(data) - const items = $('#ValidateEPG_channel_name option').toArray() - const channels = items.map(item => { - const $item = cheerio.load(item) - - return { - lang: 'id', - site_id: $item('*').attr('value'), - name: $item('*').text() - } - }) - - return channels - } -} - -function parseDescription(item) { - return cheerio.load(item)('td:last-child').text() -} - -function parseStart(item, date) { - const $ = cheerio.load(item) - let time = $('th').text() - time = `${date.format('DD/MM/YYYY')} ${time}` - - return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('td:first-of-type').text() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('table tbody tr').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'transvision.co.id', + days: 2, + url: 'https://www.transvision.co.id/jadwalacara/epg', + request: { + method: 'POST', + data: function ({ channel, date }) { + const formData = new URLSearchParams() + formData.append('ValidateEPG[channel_name]', channel.site_id) + formData.append('ValidateEPG[tanggal]', date.format('YYYY-MM-DD')) + formData.append('ValidateEPG[sinopsis]', '') + formData.append('yt0', 'PROSES') + + return formData + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + for (const item of items) { + const prev = programs[programs.length - 1] + const start = parseStart(item, date) + if (prev) prev.stop = start + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + start, + stop + }) + } + + return programs + }, + async channels() { + const data = await axios + .get('https://www.transvision.co.id/jadwalacara/epg') + .then(response => response.data) + .catch(console.log) + + const $ = cheerio.load(data) + const items = $('#ValidateEPG_channel_name option').toArray() + const channels = items.map(item => { + const $item = cheerio.load(item) + + return { + lang: 'id', + site_id: $item('*').attr('value'), + name: $item('*').text() + } + }) + + return channels + } +} + +function parseDescription(item) { + return cheerio.load(item)('td:last-child').text() +} + +function parseStart(item, date) { + const $ = cheerio.load(item) + let time = $('th').text() + time = `${date.format('DD/MM/YYYY')} ${time}` + + return dayjs.tz(time, 'DD/MM/YYYY HH:mm', 'Asia/Jakarta') +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('td:first-of-type').text() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('table tbody tr').toArray() +} diff --git a/sites/transvision.co.id/transvision.co.id.test.js b/sites/transvision.co.id/transvision.co.id.test.js index 6fc3c5d3..b04f97f7 100644 --- a/sites/transvision.co.id/transvision.co.id.test.js +++ b/sites/transvision.co.id/transvision.co.id.test.js @@ -1,82 +1,82 @@ -// npm run grab -- --site=transvision.co.id - -const { parser, url, request } = require('./transvision.co.id.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'TRIS', - xmltv_id: 'nsert.id' -} -const content = - '
    00:00:00 Insert TodayInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    01:00:00 BrownisBrownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.
    01:30:00 Warga +62Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.
    23:00:00 InsertInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    ' - -it('can generate valid url', () => { - expect(url).toBe('https://www.transvision.co.id/jadwalacara/epg') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('ValidateEPG[channel_name]')).toBe('TRIS') - expect(result.get('ValidateEPG[tanggal]')).toBe('2022-03-10') - expect(result.get('ValidateEPG[sinopsis]')).toBe('') - expect(result.get('yt0')).toBe('PROSES') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Insert Today', - description: - 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', - start: '2022-03-09T17:00:00.000Z', - stop: '2022-03-09T18:00:00.000Z' - }, - { - title: 'Brownis', - description: - 'Brownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.', - start: '2022-03-09T18:00:00.000Z', - stop: '2022-03-09T18:30:00.000Z' - }, - { - title: 'Warga +62', - description: - 'Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.', - start: '2022-03-09T18:30:00.000Z', - stop: '2022-03-10T16:00:00.000Z' - }, - { - title: 'Insert', - description: - 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', - start: '2022-03-10T16:00:00.000Z', - stop: '2022-03-10T16:30:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=transvision.co.id + +const { parser, url, request } = require('./transvision.co.id.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'TRIS', + xmltv_id: 'nsert.id' +} +const content = + '
    00:00:00 Insert TodayInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    01:00:00 BrownisBrownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.
    01:30:00 Warga +62Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.
    23:00:00 InsertInsert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.
    ' + +it('can generate valid url', () => { + expect(url).toBe('https://www.transvision.co.id/jadwalacara/epg') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('ValidateEPG[channel_name]')).toBe('TRIS') + expect(result.get('ValidateEPG[tanggal]')).toBe('2022-03-10') + expect(result.get('ValidateEPG[sinopsis]')).toBe('') + expect(result.get('yt0')).toBe('PROSES') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Insert Today', + description: + 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', + start: '2022-03-09T17:00:00.000Z', + stop: '2022-03-09T18:00:00.000Z' + }, + { + title: 'Brownis', + description: + 'Brownis atau obrolan manis merupakan program talkshow segar yang dipandu oleh Ruben Onsu bersama Ivan Gunawan.', + start: '2022-03-09T18:00:00.000Z', + stop: '2022-03-09T18:30:00.000Z' + }, + { + title: 'Warga +62', + description: + 'Warga +62 menghadirkan trend penyebaran video/momen lucu yang juga dikenal sebagai video lucu Indonesia yang tersebar di media sosial.', + start: '2022-03-09T18:30:00.000Z', + stop: '2022-03-10T16:00:00.000Z' + }, + { + title: 'Insert', + description: + 'Insert adalah program infotainment yang menceritakan berita-berita kehidupan selebriti serta gosip-gosipnya dan disajikan secara aktual dan faktual dengan suasana yang santai.', + start: '2022-03-10T16:00:00.000Z', + stop: '2022-03-10T16:30:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js b/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js index 3806f3c0..960f5ce8 100644 --- a/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js +++ b/sites/turksatkablo.com.tr/turksatkablo.com.tr.config.js @@ -1,61 +1,61 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'turksatkablo.com.tr', - days: 2, - url: function ({ date }) { - return `https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=${date.valueOf()}` - }, - request: { - timeout: 60000 - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop(item, date) - if (prev && stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - programs.push({ - title: item.b, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.c}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() -} - -function parseStop(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.d}` - - return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() -} - -function parseItems(content, channel) { - let parsed - try { - parsed = JSON.parse(content) - } catch (error) { - return [] - } - if (!parsed || !parsed.k) return [] - const data = parsed.k.find(c => c.x == channel.site_id) - - return data ? data.p : [] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'turksatkablo.com.tr', + days: 2, + url: function ({ date }) { + return `https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=${date.valueOf()}` + }, + request: { + timeout: 60000 + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop(item, date) + if (prev && stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + programs.push({ + title: item.b, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.c}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() +} + +function parseStop(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.d}` + + return DateTime.fromFormat(time, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Istanbul' }).toUTC() +} + +function parseItems(content, channel) { + let parsed + try { + parsed = JSON.parse(content) + } catch (error) { + return [] + } + if (!parsed || !parsed.k) return [] + const data = parsed.k.find(c => c.x == channel.site_id) + + return data ? data.p : [] +} diff --git a/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js b/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js index d8faf028..f33a264f 100644 --- a/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js +++ b/sites/turksatkablo.com.tr/turksatkablo.com.tr.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=turksatkablo.com.tr - -const { parser, url } = require('./turksatkablo.com.tr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-25', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '283', xmltv_id: 'SinemaTV.tr', display_name: 'Sinema TV' } -const content = - '{"k":[{"x":283,"i":2,"n":"Sinema TV","p":[{"a":"196432597608","b":"Ölüm Ormanı","c":"01:15","d":"03:00"},{"a":"196432597628","b":"Kızım","c":"15:00","d":"17:00"},{"a": "196441294843","b":"Kaçakçı","c":"23:45","d":"03:45"}]}]}' - -it('can generate valid url', () => { - const result = url({ date }) - expect(result).toBe('https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=1635120000000') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-10-24T22:15:00.000Z', - stop: '2021-10-25T00:00:00.000Z', - title: 'Ölüm Ormanı' - }, - { - start: '2021-10-25T12:00:00.000Z', - stop: '2021-10-25T14:00:00.000Z', - title: 'Kızım' - }, - { - start: '2021-10-25T20:45:00.000Z', - stop: '2021-10-26T00:45:00.000Z', - title: 'Kaçakçı' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=turksatkablo.com.tr + +const { parser, url } = require('./turksatkablo.com.tr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-25', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '283', xmltv_id: 'SinemaTV.tr', display_name: 'Sinema TV' } +const content = + '{"k":[{"x":283,"i":2,"n":"Sinema TV","p":[{"a":"196432597608","b":"Ölüm Ormanı","c":"01:15","d":"03:00"},{"a":"196432597628","b":"Kızım","c":"15:00","d":"17:00"},{"a": "196441294843","b":"Kaçakçı","c":"23:45","d":"03:45"}]}]}' + +it('can generate valid url', () => { + const result = url({ date }) + expect(result).toBe('https://www.turksatkablo.com.tr/userUpload/EPG/y.json?_=1635120000000') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-10-24T22:15:00.000Z', + stop: '2021-10-25T00:00:00.000Z', + title: 'Ölüm Ormanı' + }, + { + start: '2021-10-25T12:00:00.000Z', + stop: '2021-10-25T14:00:00.000Z', + title: 'Kızım' + }, + { + start: '2021-10-25T20:45:00.000Z', + stop: '2021-10-26T00:45:00.000Z', + title: 'Kaçakçı' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.blue.ch/tv.blue.ch.config.js b/sites/tv.blue.ch/tv.blue.ch.config.js index 8c130b49..c1c72a90 100644 --- a/sites/tv.blue.ch/tv.blue.ch.config.js +++ b/sites/tv.blue.ch/tv.blue.ch.config.js @@ -1,85 +1,85 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'tv.blue.ch', - days: 2, - url: function ({ channel, date }) { - return `https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=${ - channel.site_id - };start=${date.format('YYYYMMDDHHss')};end=${date - .add(1, 'd') - .format('YYYYMMDDHHss')};level=normal)` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const title = parseTitle(item) - if (title === 'Sendepause') return - programs.push({ - title, - description: parseDescription(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get('https://services.sg101.prd.sctv.ch/portfolio/tv/channels') - .then(r => r.data) - .catch(console.log) - - return items.map(item => { - return { - lang: item.Languages[0] || 'de', - site_id: item.Identifier, - name: item.Title - } - }) - } -} - -function parseTitle(item) { - return item.Content.Description.Title -} - -function parseDescription(item) { - return item.Content.Description.Summary -} - -function parseIcon(item) { - const image = item.Content.Nodes ? item.Content.Nodes.Items.find(i => i.Kind === 'Image') : null - const path = image ? image.ContentPath : null - - return path ? `https://services.sg101.prd.sctv.ch/content/images${path}_w1920.webp` : null -} - -function parseStart(item) { - const available = item.Availabilities.length ? item.Availabilities[0] : null - - return dayjs(available.AvailabilityStart) -} - -function parseStop(item) { - const available = item.Availabilities.length ? item.Availabilities[0] : null - - return dayjs(available.AvailabilityEnd) -} - -function parseItems(content) { - const data = JSON.parse(content) - const nodes = data.Nodes.Items.filter(i => i.Kind === 'Channel') - if (!nodes.length) return [] - - return nodes[0].Content.Nodes && Array.isArray(nodes[0].Content.Nodes.Items) - ? nodes[0].Content.Nodes.Items.filter(i => i.Kind === 'Broadcast') - : [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'tv.blue.ch', + days: 2, + url: function ({ channel, date }) { + return `https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=${ + channel.site_id + };start=${date.format('YYYYMMDDHHss')};end=${date + .add(1, 'd') + .format('YYYYMMDDHHss')};level=normal)` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const title = parseTitle(item) + if (title === 'Sendepause') return + programs.push({ + title, + description: parseDescription(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get('https://services.sg101.prd.sctv.ch/portfolio/tv/channels') + .then(r => r.data) + .catch(console.log) + + return items.map(item => { + return { + lang: item.Languages[0] || 'de', + site_id: item.Identifier, + name: item.Title + } + }) + } +} + +function parseTitle(item) { + return item.Content.Description.Title +} + +function parseDescription(item) { + return item.Content.Description.Summary +} + +function parseIcon(item) { + const image = item.Content.Nodes ? item.Content.Nodes.Items.find(i => i.Kind === 'Image') : null + const path = image ? image.ContentPath : null + + return path ? `https://services.sg101.prd.sctv.ch/content/images${path}_w1920.webp` : null +} + +function parseStart(item) { + const available = item.Availabilities.length ? item.Availabilities[0] : null + + return dayjs(available.AvailabilityStart) +} + +function parseStop(item) { + const available = item.Availabilities.length ? item.Availabilities[0] : null + + return dayjs(available.AvailabilityEnd) +} + +function parseItems(content) { + const data = JSON.parse(content) + const nodes = data.Nodes.Items.filter(i => i.Kind === 'Channel') + if (!nodes.length) return [] + + return nodes[0].Content.Nodes && Array.isArray(nodes[0].Content.Nodes.Items) + ? nodes[0].Content.Nodes.Items.filter(i => i.Kind === 'Broadcast') + : [] +} diff --git a/sites/tv.blue.ch/tv.blue.ch.test.js b/sites/tv.blue.ch/tv.blue.ch.test.js index 73391dde..873ae182 100644 --- a/sites/tv.blue.ch/tv.blue.ch.test.js +++ b/sites/tv.blue.ch/tv.blue.ch.test.js @@ -1,76 +1,76 @@ -// node ./scripts/commands/parse-channels.js --config=./sites/tv.blue.ch/tv.blue.ch.config.js --output=./sites/tv.blue.ch/tv.blue.ch.channels.xml -// npm run grab -- --site=tv.blue.ch - -const { parser, url } = require('./tv.blue.ch.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1221', - xmltv_id: 'BlueZoomD.ch' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)' - ) -}) - -it('can parse response', () => { - const content = - '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45","Kind":"Broadcast","Channel":"1221","Content":{"Description":{"Title":"Weekend on the Rocks","Summary":" - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.","ShortSummary":"","Country":"CH","ReleaseDate":"2021-01-01T00:00:00Z","Source":"13","Language":"de","Duration":"00:30:00"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45_landscape","Kind":"Image","Role":"Landscape","ContentPath":"/tv/broadcast/1221/t1221ddc59247d45_landscape","Version":{"Date":"2022-01-04T08:55:22.567Z"}}]},"TechnicalAttributes":{"Stereo":true}},"Version":{"Hash":"60d3"},"Availabilities":[{"AvailabilityStart":"2022-01-16T23:30:00Z","AvailabilityEnd":"2022-01-17T00:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"2b0898c7-3920-3200-7048-4ea5d9138921"},{"Domain":"TV","Kind":"Reference","Role":"OriginalAirSeries","TargetIdentifier":"false"},{"Domain":"TV","Kind":"Reference","Role":"ExternalBroadcastIdentifier","TargetIdentifier":"167324536-11"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p12211351631155","Title":"Original"}]}]}}}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-16T23:30:00.000Z', - stop: '2022-01-17T00:00:00.000Z', - title: 'Weekend on the Rocks', - description: - ' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.', - icon: 'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp' - } - ]) -}) - -it('can parse response without image', () => { - const content = - '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t10014a78a8b0668","Kind":"Broadcast","Channel":"1001","Content":{"Description":{"Title":"Lorem ipsum","Language":"fr","Duration":"00:01:00"}},"Version":{"Hash":"440e"},"Availabilities":[{"AvailabilityStart":"2022-01-17T04:59:00Z","AvailabilityEnd":"2022-01-17T05:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"3553a4f2-ff63-5200-7048-d8d59d805f81"},{"Domain":"TV","Kind":"Reference","Role":"Dummy","TargetIdentifier":"True"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p1"}]}]}}}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-17T04:59:00.000Z', - stop: '2022-01-17T05:00:00.000Z', - title: 'Lorem ipsum' - } - ]) -}) - -it('can handle wrong site id', () => { - const result = parser({ - content: - '{"Status":{"Version":"7","Status":"OK","ProcessingTime":"00:00:00.0160674","ExecutionTime":"2022-01-17T13:47:30.584Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=12210;start=202201170000;end=202201180000;level=normal)","Identifiers":["12210"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117114748","DbCreationTime":"2022-01-17T11:49:14.608Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Items":[]}}' - }) - expect(result).toMatchObject([]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: - '{"Status":{"Version":"7","Status":"OK","ExecutionTime":"2022-01-17T15:30:37.97Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=1884;start=202201170000;end=202201180000;level=normal)","Identifiers":["1884"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117144354","DbCreationTime":"2022-01-17T14:45:11.84Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1884","Kind":"Channel","Content":{"Description":{"Title":"Fisu.tv 1","Language":"en"}}}]}}' - }) - expect(result).toMatchObject([]) -}) +// node ./scripts/commands/parse-channels.js --config=./sites/tv.blue.ch/tv.blue.ch.config.js --output=./sites/tv.blue.ch/tv.blue.ch.channels.xml +// npm run grab -- --site=tv.blue.ch + +const { parser, url } = require('./tv.blue.ch.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1221', + xmltv_id: 'BlueZoomD.ch' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://services.sg101.prd.sctv.ch/catalog/tv/channels/list/(ids=1221;start=202201170000;end=202201180000;level=normal)' + ) +}) + +it('can parse response', () => { + const content = + '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45","Kind":"Broadcast","Channel":"1221","Content":{"Description":{"Title":"Weekend on the Rocks","Summary":" - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.","ShortSummary":"","Country":"CH","ReleaseDate":"2021-01-01T00:00:00Z","Source":"13","Language":"de","Duration":"00:30:00"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"t1221ddc59247d45_landscape","Kind":"Image","Role":"Landscape","ContentPath":"/tv/broadcast/1221/t1221ddc59247d45_landscape","Version":{"Date":"2022-01-04T08:55:22.567Z"}}]},"TechnicalAttributes":{"Stereo":true}},"Version":{"Hash":"60d3"},"Availabilities":[{"AvailabilityStart":"2022-01-16T23:30:00Z","AvailabilityEnd":"2022-01-17T00:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"2b0898c7-3920-3200-7048-4ea5d9138921"},{"Domain":"TV","Kind":"Reference","Role":"OriginalAirSeries","TargetIdentifier":"false"},{"Domain":"TV","Kind":"Reference","Role":"ExternalBroadcastIdentifier","TargetIdentifier":"167324536-11"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p12211351631155","Title":"Original"}]}]}}}]}}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-16T23:30:00.000Z', + stop: '2022-01-17T00:00:00.000Z', + title: 'Weekend on the Rocks', + description: + ' - «R.E.S.P.E.C.T», lieber Charles Nguela. Der Comedian tourt fleissig durch die Schweiz, macht für uns aber einen Halt, um in der neuen Ausgabe von «Weekend on the Rocks» mit Moderatorin Vania Spescha über die Entertainment-News der Woche zu plaudern.', + icon: 'https://services.sg101.prd.sctv.ch/content/images/tv/broadcast/1221/t1221ddc59247d45_landscape_w1920.webp' + } + ]) +}) + +it('can parse response without image', () => { + const content = + '{"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1221","Kind":"Channel","Content":{"Description":{"Title":"blue Zoom D","Language":"de"},"Nodes":{"Count":29,"TotalItemCount":29,"Items":[{"Domain":"TV","Identifier":"t10014a78a8b0668","Kind":"Broadcast","Channel":"1001","Content":{"Description":{"Title":"Lorem ipsum","Language":"fr","Duration":"00:01:00"}},"Version":{"Hash":"440e"},"Availabilities":[{"AvailabilityStart":"2022-01-17T04:59:00Z","AvailabilityEnd":"2022-01-17T05:00:00Z"}],"Relations":[{"Domain":"TV","Kind":"Reference","Role":"ChannelIdentifier","TargetIdentifier":"3553a4f2-ff63-5200-7048-d8d59d805f81"},{"Domain":"TV","Kind":"Reference","Role":"Dummy","TargetIdentifier":"True"},{"Domain":"TV","Kind":"Reference","Role":"ProgramIdentifier","TargetIdentifier":"p1"}]}]}}}]}}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-17T04:59:00.000Z', + stop: '2022-01-17T05:00:00.000Z', + title: 'Lorem ipsum' + } + ]) +}) + +it('can handle wrong site id', () => { + const result = parser({ + content: + '{"Status":{"Version":"7","Status":"OK","ProcessingTime":"00:00:00.0160674","ExecutionTime":"2022-01-17T13:47:30.584Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=12210;start=202201170000;end=202201180000;level=normal)","Identifiers":["12210"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117114748","DbCreationTime":"2022-01-17T11:49:14.608Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Items":[]}}' + }) + expect(result).toMatchObject([]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: + '{"Status":{"Version":"7","Status":"OK","ExecutionTime":"2022-01-17T15:30:37.97Z"},"Request":{"Domain":"TV","Resource":"Channels","Action":"List","Parameters":"(ids=1884;start=202201170000;end=202201180000;level=normal)","Identifiers":["1884"],"Start":"2022-01-17T00:00:00Z","End":"2022-01-18T00:00:00Z","DataLevel":"Normal"},"DataSource":{"Snapshot":"Tv_20220117144354","DbCreationTime":"2022-01-17T14:45:11.84Z","IncrementCreationTime":"0001-01-01T00:00:00Z"},"Nodes":{"Count":1,"TotalItemCount":1,"Items":[{"Domain":"TV","Identifier":"1884","Kind":"Channel","Content":{"Description":{"Title":"Fisu.tv 1","Language":"en"}}}]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.cctv.com/tv.cctv.com.config.js b/sites/tv.cctv.com/tv.cctv.com.config.js index 8f2aa3b4..ad803161 100644 --- a/sites/tv.cctv.com/tv.cctv.com.config.js +++ b/sites/tv.cctv.com/tv.cctv.com.config.js @@ -1,41 +1,41 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.cctv.com', - days: 2, - url({ channel, date }) { - return `https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=${ - channel.site_id - }&d=${date.format('YYYYMMDD')}` - }, - parser({ content, channel }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const title = item.title - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title, - start, - stop - }) - }) - - return programs - } -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - - return data.data[channel.site_id].list || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.cctv.com', + days: 2, + url({ channel, date }) { + return `https://api.cntv.cn/epg/getEpgInfoByChannelNew?serviceId=tvcctv&c=${ + channel.site_id + }&d=${date.format('YYYYMMDD')}` + }, + parser({ content, channel }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const title = item.title + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title, + start, + stop + }) + }) + + return programs + } +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + + return data.data[channel.site_id].list || [] +} diff --git a/sites/tv.dir.bg/tv.dir.bg.config.js b/sites/tv.dir.bg/tv.dir.bg.config.js index e43f7563..c1a15fbf 100644 --- a/sites/tv.dir.bg/tv.dir.bg.config.js +++ b/sites/tv.dir.bg/tv.dir.bg.config.js @@ -1,88 +1,88 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tv.dir.bg', - days: 2, - url({ channel, date }) { - return `https://tv.dir.bg/tv_channel.php?id=${channel.site_id}&dd=${date.format('DD.MM')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (!start) return - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const requests = [ - axios.get('https://tv.dir.bg/programata.php?t=0'), - axios.get('https://tv.dir.bg/programata.php?t=1') - ] - - const items = await Promise.all(requests) - .then(r => { - return r - .map(i => { - const html = i.data - const $ = cheerio.load(html) - return $('#programa-left > div > div > div > a').toArray() - }) - .reduce((acc, curr) => { - acc = acc.concat(curr) - return acc - }, []) - }) - .catch(console.log) - - const $ = cheerio.load('') - return items.map(item => { - const $item = $(item) - return { - lang: 'bg', - site_id: $item.attr('href').replace('tv_channel.php?id=', ''), - name: $item.find('div.thumbnail > img').attr('alt') - } - }) - } -} - -function parseStart($item, date) { - const time = $item('i').text() - if (!time) return null - const dateString = `${date.format('MM/DD/YYYY')} ${time}` - - return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Sofia' }).toUTC() -} - -function parseTitle($item) { - return $item - .text() - .replace(/^\d{2}.\d{2}/, '') - .trim() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#events > li').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tv.dir.bg', + days: 2, + url({ channel, date }) { + return `https://tv.dir.bg/tv_channel.php?id=${channel.site_id}&dd=${date.format('DD.MM')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (!start) return + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const requests = [ + axios.get('https://tv.dir.bg/programata.php?t=0'), + axios.get('https://tv.dir.bg/programata.php?t=1') + ] + + const items = await Promise.all(requests) + .then(r => { + return r + .map(i => { + const html = i.data + const $ = cheerio.load(html) + return $('#programa-left > div > div > div > a').toArray() + }) + .reduce((acc, curr) => { + acc = acc.concat(curr) + return acc + }, []) + }) + .catch(console.log) + + const $ = cheerio.load('') + return items.map(item => { + const $item = $(item) + return { + lang: 'bg', + site_id: $item.attr('href').replace('tv_channel.php?id=', ''), + name: $item.find('div.thumbnail > img').attr('alt') + } + }) + } +} + +function parseStart($item, date) { + const time = $item('i').text() + if (!time) return null + const dateString = `${date.format('MM/DD/YYYY')} ${time}` + + return DateTime.fromFormat(dateString, 'MM/dd/yyyy HH.mm', { zone: 'Europe/Sofia' }).toUTC() +} + +function parseTitle($item) { + return $item + .text() + .replace(/^\d{2}.\d{2}/, '') + .trim() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#events > li').toArray() +} diff --git a/sites/tv.dir.bg/tv.dir.bg.test.js b/sites/tv.dir.bg/tv.dir.bg.test.js index 4b0074e8..0293438e 100644 --- a/sites/tv.dir.bg/tv.dir.bg.test.js +++ b/sites/tv.dir.bg/tv.dir.bg.test.js @@ -1,57 +1,57 @@ -// node ./scripts/commands/parse-channels.js --config=./sites/tv.dir.bg/tv.dir.bg.config.js --output=./sites/tv.dir.bg/tv.dir.bg.channels.xml -// npm run grab -- --site=tv.dir.bg - -const { parser, url } = require('./tv.dir.bg.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '12', - xmltv_id: 'BTV.bg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv.dir.bg/tv_channel.php?id=12&dd=20.01') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-20T04:00:00.000Z', - stop: '2022-01-20T13:00:00.000Z', - title: '„Тази сутрин” - информационно предаване с водещи Златимир Йочеви Биляна Гавазова' - }, - { - start: '2022-01-20T13:00:00.000Z', - stop: '2022-01-21T03:30:00.000Z', - title: '„Доктор Чудо” - сериал, еп.71' - }, - { - start: '2022-01-21T03:30:00.000Z', - stop: '2022-01-21T04:00:00.000Z', - title: '„Лице в лице” /п./' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '
      ' - }) - expect(result).toMatchObject([]) -}) +// node ./scripts/commands/parse-channels.js --config=./sites/tv.dir.bg/tv.dir.bg.config.js --output=./sites/tv.dir.bg/tv.dir.bg.channels.xml +// npm run grab -- --site=tv.dir.bg + +const { parser, url } = require('./tv.dir.bg.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '12', + xmltv_id: 'BTV.bg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv.dir.bg/tv_channel.php?id=12&dd=20.01') +}) + +it('can parse response', () => { + const content = + '' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-20T04:00:00.000Z', + stop: '2022-01-20T13:00:00.000Z', + title: '„Тази сутрин” - информационно предаване с водещи Златимир Йочеви Биляна Гавазова' + }, + { + start: '2022-01-20T13:00:00.000Z', + stop: '2022-01-21T03:30:00.000Z', + title: '„Доктор Чудо” - сериал, еп.71' + }, + { + start: '2022-01-21T03:30:00.000Z', + stop: '2022-01-21T04:00:00.000Z', + title: '„Лице в лице” /п./' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '
        ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.lv/tv.lv.config.js b/sites/tv.lv/tv.lv.config.js index 985c8270..fa6549e5 100644 --- a/sites/tv.lv/tv.lv.config.js +++ b/sites/tv.lv/tv.lv.config.js @@ -1,43 +1,43 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.lv', - days: 2, - url: function ({ date, channel }) { - return `https://www.tv.lv/programme/listing/none/${date.format( - 'DD-MM-YYYY' - )}?filter=channel&subslug=${channel.site_id}` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title: item.title, - description: item.description_long, - category: item.categorystring, - icon: item.image, - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item) { - return item.start_unix ? dayjs.unix(item.start_unix) : null -} - -function parseStop(item) { - return item.stop_unix ? dayjs.unix(item.stop_unix) : null -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.schedule.programme || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.lv', + days: 2, + url: function ({ date, channel }) { + return `https://www.tv.lv/programme/listing/none/${date.format( + 'DD-MM-YYYY' + )}?filter=channel&subslug=${channel.site_id}` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title: item.title, + description: item.description_long, + category: item.categorystring, + icon: item.image, + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item) { + return item.start_unix ? dayjs.unix(item.start_unix) : null +} + +function parseStop(item) { + return item.stop_unix ? dayjs.unix(item.stop_unix) : null +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.schedule.programme || [] +} diff --git a/sites/tv.mail.ru/tv.mail.ru.config.js b/sites/tv.mail.ru/tv.mail.ru.config.js index 2082a21e..32211404 100644 --- a/sites/tv.mail.ru/tv.mail.ru.config.js +++ b/sites/tv.mail.ru/tv.mail.ru.config.js @@ -1,76 +1,76 @@ -const { DateTime } = require('luxon') - -module.exports = { - site: 'tv.mail.ru', - days: 2, - delay: 1000, - url({ channel, date }) { - return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${ - channel.site_id - }&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ hours: 1 }) - programs.push({ - title: item.name, - category: parseCategory(item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.start}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC() -} - -function parseCategory(item) { - const categories = { - 1: 'Фильм', - 2: 'Сериал', - 6: 'Документальное', - 7: 'Телемагазин', - 8: 'Позновательное', - 10: 'Другое', - 14: 'ТВ-шоу', - 16: 'Досуг,Хобби', - 17: 'Ток-шоу', - 18: 'Юмористическое', - 23: 'Музыка', - 24: 'Развлекательное', - 25: 'Игровое', - 26: 'Новости' - } - - return categories[item.category_id] - ? { - lang: 'ru', - value: categories[item.category_id] - } - : null -} - -function parseItems(content) { - const json = JSON.parse(content) - if (!Array.isArray(json.schedule) || !json.schedule[0]) return [] - const event = json.schedule[0].event || [] - - return [...event.past, ...event.current] -} +const { DateTime } = require('luxon') + +module.exports = { + site: 'tv.mail.ru', + days: 2, + delay: 1000, + url({ channel, date }) { + return `https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=${ + channel.site_id + }&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ hours: 1 }) + programs.push({ + title: item.name, + category: parseCategory(item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.start}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Europe/Moscow' }).toUTC() +} + +function parseCategory(item) { + const categories = { + 1: 'Фильм', + 2: 'Сериал', + 6: 'Документальное', + 7: 'Телемагазин', + 8: 'Позновательное', + 10: 'Другое', + 14: 'ТВ-шоу', + 16: 'Досуг,Хобби', + 17: 'Ток-шоу', + 18: 'Юмористическое', + 23: 'Музыка', + 24: 'Развлекательное', + 25: 'Игровое', + 26: 'Новости' + } + + return categories[item.category_id] + ? { + lang: 'ru', + value: categories[item.category_id] + } + : null +} + +function parseItems(content) { + const json = JSON.parse(content) + if (!Array.isArray(json.schedule) || !json.schedule[0]) return [] + const event = json.schedule[0].event || [] + + return [...event.past, ...event.current] +} diff --git a/sites/tv.mail.ru/tv.mail.ru.test.js b/sites/tv.mail.ru/tv.mail.ru.test.js index a77b4323..f8797a21 100644 --- a/sites/tv.mail.ru/tv.mail.ru.test.js +++ b/sites/tv.mail.ru/tv.mail.ru.test.js @@ -1,79 +1,79 @@ -// npm run grab -- --site=tv.mail.ru - -const { parser, url } = require('./tv.mail.ru.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2785', - xmltv_id: '21TV.am' -} -const content = - '{"status":"OK","schedule":[{"channel":{"name":"21TV","pic_url":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png","pic_url_128":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACwjJ45j9sTP8fcjPJnJ4xk5e_ILr5iXwjLMhWhzlVnIJkrtT42vEp9walcgpXRKDq9KFoliEPR0xI-LEh96C_izY.png","pic_url_64":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/dpr:200/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png"},"event":{"current":[{"channel_id":"2785","name":"Պրոֆեսիոնալները","category_id":8,"episode_title":"","url":"/moskva/channel/2785/173593246/","id":"173593246","start":"02:40","episode_num":0},{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593142/","id":"173593142","start":"03:25","episode_num":0}],"past":[{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593328/","id":"173593328","start":"23:35","episode_num":0},{"channel_id":"2785","video":{"currency":"RUB","price_min":"249.00","price_txt":"249 р."},"name":"Վերջին թագավորությունը","category_id":2,"episode_title":"","url":"/moskva/channel/2785/173593318/","id":"173593318","start":"01:40","our_event_id":"890224","episode_num":0}]}}]}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24' - ) -}) - -it('can parse response', () => { - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T20:35:00.000Z', - stop: '2021-11-24T22:40:00.000Z', - title: 'Նոնստոպ․ Տեսահոլովակներ', - category: { - lang: 'ru', - value: 'Музыка' - } - }, - { - start: '2021-11-24T22:40:00.000Z', - stop: '2021-11-24T23:40:00.000Z', - title: 'Վերջին թագավորությունը', - category: { - lang: 'ru', - value: 'Сериал' - } - }, - { - start: '2021-11-24T23:40:00.000Z', - stop: '2021-11-25T00:25:00.000Z', - title: 'Պրոֆեսիոնալները', - category: { - lang: 'ru', - value: 'Позновательное' - } - }, - { - start: '2021-11-25T00:25:00.000Z', - stop: '2021-11-25T01:25:00.000Z', - title: 'Նոնստոպ․ Տեսահոլովակներ', - category: { - lang: 'ru', - value: 'Музыка' - } - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"status":"OK","current_ts":1637788593,"form":{"values":[]},"current_offset":10800,"schedule":[{"channel":null,"event":{"current":[],"past":[]}}]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tv.mail.ru + +const { parser, url } = require('./tv.mail.ru.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2785', + xmltv_id: '21TV.am' +} +const content = + '{"status":"OK","schedule":[{"channel":{"name":"21TV","pic_url":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png","pic_url_128":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/AAACwjJ45j9sTP8fcjPJnJ4xk5e_ILr5iXwjLMhWhzlVnIJkrtT42vEp9walcgpXRKDq9KFoliEPR0xI-LEh96C_izY.png","pic_url_64":"https://resizer.mail.ru/p/1234c5ac-c19c-5cf2-9c6a-fc0efca920ac/dpr:200/AAACm2w9aDpGPSWXzsH7PBq2X3I6pbxqmrj-yeuVppAKyyBHXE_dH_7pHQ2rOavyKiC4iHIWTab9SeKo7pKgr71lqVA.png"},"event":{"current":[{"channel_id":"2785","name":"Պրոֆեսիոնալները","category_id":8,"episode_title":"","url":"/moskva/channel/2785/173593246/","id":"173593246","start":"02:40","episode_num":0},{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593142/","id":"173593142","start":"03:25","episode_num":0}],"past":[{"channel_id":"2785","name":"Նոնստոպ․ Տեսահոլովակներ","category_id":23,"episode_title":"","url":"/moskva/channel/2785/173593328/","id":"173593328","start":"23:35","episode_num":0},{"channel_id":"2785","video":{"currency":"RUB","price_min":"249.00","price_txt":"249 р."},"name":"Վերջին թագավորությունը","category_id":2,"episode_title":"","url":"/moskva/channel/2785/173593318/","id":"173593318","start":"01:40","our_event_id":"890224","episode_num":0}]}}]}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.mail.ru/ajax/channel/?region_id=70&channel_id=2785&date=2021-11-24' + ) +}) + +it('can parse response', () => { + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T20:35:00.000Z', + stop: '2021-11-24T22:40:00.000Z', + title: 'Նոնստոպ․ Տեսահոլովակներ', + category: { + lang: 'ru', + value: 'Музыка' + } + }, + { + start: '2021-11-24T22:40:00.000Z', + stop: '2021-11-24T23:40:00.000Z', + title: 'Վերջին թագավորությունը', + category: { + lang: 'ru', + value: 'Сериал' + } + }, + { + start: '2021-11-24T23:40:00.000Z', + stop: '2021-11-25T00:25:00.000Z', + title: 'Պրոֆեսիոնալները', + category: { + lang: 'ru', + value: 'Позновательное' + } + }, + { + start: '2021-11-25T00:25:00.000Z', + stop: '2021-11-25T01:25:00.000Z', + title: 'Նոնստոպ․ Տեսահոլովակներ', + category: { + lang: 'ru', + value: 'Музыка' + } + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"status":"OK","current_ts":1637788593,"form":{"values":[]},"current_offset":10800,"schedule":[{"channel":null,"event":{"current":[],"past":[]}}]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js b/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js index 0595bf45..1d0a83ce 100644 --- a/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js +++ b/sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js @@ -1,57 +1,57 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'tv.movistar.com.pe', - days: 2, - url({ channel, date }) { - return `https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=${date.unix()}&endtime=${date - .add(1, 'd') - .unix()}&livechannelpids=${channel.site_id}` - }, - parser({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.Title, - description: item.Description, - icon: parseIcon(item), - start: parseTime(item.Start), - stop: parseTime(item.End) - }) - }) - - return programs - }, - async channels() { - const items = await axios - .get( - 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/contents/all?contentTypes=LCH&fields=Pid,Name&orderBy=contentOrder&limit=1000' - ) - .then(r => r.data.Content.List) - .catch(console.error) - - return items.map(i => { - return { - lang: 'es', - name: i.Name, - site_id: i.Pid.toLowerCase() - } - }) - } -} - -function parseIcon(item) { - return item.Images?.VideoFrame?.[0]?.Url -} - -function parseTime(timestamp) { - return dayjs.unix(timestamp) -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.Content || [] -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'tv.movistar.com.pe', + days: 2, + url({ channel, date }) { + return `https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=${date.unix()}&endtime=${date + .add(1, 'd') + .unix()}&livechannelpids=${channel.site_id}` + }, + parser({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.Title, + description: item.Description, + icon: parseIcon(item), + start: parseTime(item.Start), + stop: parseTime(item.End) + }) + }) + + return programs + }, + async channels() { + const items = await axios + .get( + 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/contents/all?contentTypes=LCH&fields=Pid,Name&orderBy=contentOrder&limit=1000' + ) + .then(r => r.data.Content.List) + .catch(console.error) + + return items.map(i => { + return { + lang: 'es', + name: i.Name, + site_id: i.Pid.toLowerCase() + } + }) + } +} + +function parseIcon(item) { + return item.Images?.VideoFrame?.[0]?.Url +} + +function parseTime(timestamp) { + return dayjs.unix(timestamp) +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.Content || [] +} diff --git a/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js b/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js index ac12544a..28c957ca 100644 --- a/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js +++ b/sites/tv.movistar.com.pe/tv.movistar.com.pe.test.js @@ -1,47 +1,47 @@ -// npm run channels:parse -- --config=./sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js --output=./sites/tv.movistar.com.pe/tv.movistar.com.pe.channels.xml -// npm run grab -- --site=tv.movistar.com.pe - -const { parser, url } = require('./tv.movistar.com.pe.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'lch2219', - xmltv_id: 'WillaxTV.pe' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=1669680000&endtime=1669766400&livechannelpids=lch2219' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-28T23:50:00.000Z', - stop: '2022-11-29T00:50:00.000Z', - title: 'Willax noticias edición central', - description: - 'Edición central con el desarrollo y cobertura noticiosa de todos los acontecimientos nacionales e internacionales.', - icon: 'http://media.gvp.telefonica.com/storagearea0/IMAGES/00/13/00/13003906_281B2DAB18B01955.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const result = parser({ content, channel, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.movistar.com.pe/tv.movistar.com.pe.config.js --output=./sites/tv.movistar.com.pe/tv.movistar.com.pe.channels.xml +// npm run grab -- --site=tv.movistar.com.pe + +const { parser, url } = require('./tv.movistar.com.pe.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'lch2219', + xmltv_id: 'WillaxTV.pe' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://contentapi-pe.cdn.telefonica.com/28/default/es-PE/schedules?fields=Pid,Title,Description,ChannelName,LiveChannelPid,Start,End,images.videoFrame,AgeRatingPid&orderBy=START_TIME%3Aa&filteravailability=false&starttime=1669680000&endtime=1669766400&livechannelpids=lch2219' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-28T23:50:00.000Z', + stop: '2022-11-29T00:50:00.000Z', + title: 'Willax noticias edición central', + description: + 'Edición central con el desarrollo y cobertura noticiosa de todos los acontecimientos nacionales e internacionales.', + icon: 'http://media.gvp.telefonica.com/storagearea0/IMAGES/00/13/00/13003906_281B2DAB18B01955.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const result = parser({ content, channel, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.nu/tv.nu.config.js b/sites/tv.nu/tv.nu.config.js index 2469fe22..a0c5d91e 100644 --- a/sites/tv.nu/tv.nu.config.js +++ b/sites/tv.nu/tv.nu.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.nu', - days: 2, - url: function ({ channel, date }) { - return `https://web-api.tv.nu/channels/${channel.site_id}/schedule?date=${date.format( - 'YYYY-MM-DD' - )}&fullDay=true` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - icon: item.imageLandscape, - category: item.genres, - season: item.seasonNumber || null, - episode: item.episodeNumber || null, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - if (!item.broadcast || !item.broadcast.startTime) return null - - return dayjs(item.broadcast.startTime) -} - -function parseStop(item) { - if (!item.broadcast || !item.broadcast.endTime) return null - - return dayjs(item.broadcast.endTime) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !data.data || !Array.isArray(data.data.broadcasts)) return [] - - return data.data.broadcasts -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.nu', + days: 2, + url: function ({ channel, date }) { + return `https://web-api.tv.nu/channels/${channel.site_id}/schedule?date=${date.format( + 'YYYY-MM-DD' + )}&fullDay=true` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + icon: item.imageLandscape, + category: item.genres, + season: item.seasonNumber || null, + episode: item.episodeNumber || null, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + if (!item.broadcast || !item.broadcast.startTime) return null + + return dayjs(item.broadcast.startTime) +} + +function parseStop(item) { + if (!item.broadcast || !item.broadcast.endTime) return null + + return dayjs(item.broadcast.endTime) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !data.data || !Array.isArray(data.data.broadcasts)) return [] + + return data.data.broadcasts +} diff --git a/sites/tv.nu/tv.nu.test.js b/sites/tv.nu/tv.nu.test.js index af881a5f..5170f29a 100644 --- a/sites/tv.nu/tv.nu.test.js +++ b/sites/tv.nu/tv.nu.test.js @@ -1,51 +1,51 @@ -// npm run grab -- --site=tv.nu - -const { parser, url } = require('./tv.nu.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '3sat', - xmltv_id: '3sat.de' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://web-api.tv.nu/channels/3sat/schedule?date=2022-03-06&fullDay=true' - ) -}) - -it('can parse response', () => { - const content = - '{"meta":{"status":200},"data":{"id":30139,"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}},"broadcasts":[{"type":"broadcast","id":"1OoSZY-7Q7-1DzQ","slug":"csi","programId":"2452","isPlay":true,"isMovie":false,"isSeries":true,"isLive":false,"title":"CSI: Den Tätern auf der Spur","description":"Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.","imagePortrait":"https://new.static.tv.nu/16686512","imageLandscape":"https://new.static.tv.nu/13119997","year":2006,"genres":["Action","Kriminaldrama","Mysterium","Spänning","Thriller"],"imdb":{"rating":"7.7","link":"https://www.imdb.com/title/tt0247082"},"playProviders":[{"name":"Viaplay","slug":"viaplay","themedLogo":{"light":{"url":"https://new.static.tv.nu/17048879","isFallback":false},"dark":{"url":"https://new.static.tv.nu/119659437","isFallback":false}},"url":"https://viaplay.se/serier/csi-crime-scene-investigation/sasong-6/avsnitt-19?utm_source=tv.nu&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Tele2 Play","slug":"tele2play","themedLogo":{"light":{"url":"https://new.static.tv.nu/158747195","isFallback":false},"dark":{"url":"https://new.static.tv.nu/158747194","isFallback":false}},"url":"https://www.comhemplay.se/open/vod/SH016259780000?utm_source=tv.nu&utm_medium=partner&utm_campaign=tabla&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Prime Video","slug":"prime-video","themedLogo":{"light":{"url":"https://new.static.tv.nu/23085972","isFallback":false},"dark":{"url":"https://new.static.tv.nu/275111","isFallback":true}},"url":"https://app.primevideo.com/detail?gti=amzn1.dv.gti.54af67f9-e58f-e6db-4991-81eb4f2efa37&utm_source=tv.nu"}],"broadcast":{"id":"1OoSZY-7Q7-1DzQ","startTime":1660878900000,"endTime":1660881600000,"channel":{"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}}}},"totalEpisodes":24,"episodeNumber":19,"seasonNumber":6}]}}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-08-19T03:15:00.000Z', - stop: '2022-08-19T04:00:00.000Z', - title: 'CSI: Den Tätern auf der Spur', - description: - 'Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.', - icon: 'https://new.static.tv.nu/13119997', - category: ['Action', 'Kriminaldrama', 'Mysterium', 'Spänning', 'Thriller'], - season: 6, - episode: 19 - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"meta":{"status":200},"data":{"broadcasts":[]}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tv.nu + +const { parser, url } = require('./tv.nu.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '3sat', + xmltv_id: '3sat.de' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://web-api.tv.nu/channels/3sat/schedule?date=2022-03-06&fullDay=true' + ) +}) + +it('can parse response', () => { + const content = + '{"meta":{"status":200},"data":{"id":30139,"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}},"broadcasts":[{"type":"broadcast","id":"1OoSZY-7Q7-1DzQ","slug":"csi","programId":"2452","isPlay":true,"isMovie":false,"isSeries":true,"isLive":false,"title":"CSI: Den Tätern auf der Spur","description":"Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.","imagePortrait":"https://new.static.tv.nu/16686512","imageLandscape":"https://new.static.tv.nu/13119997","year":2006,"genres":["Action","Kriminaldrama","Mysterium","Spänning","Thriller"],"imdb":{"rating":"7.7","link":"https://www.imdb.com/title/tt0247082"},"playProviders":[{"name":"Viaplay","slug":"viaplay","themedLogo":{"light":{"url":"https://new.static.tv.nu/17048879","isFallback":false},"dark":{"url":"https://new.static.tv.nu/119659437","isFallback":false}},"url":"https://viaplay.se/serier/csi-crime-scene-investigation/sasong-6/avsnitt-19?utm_source=tv.nu&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Tele2 Play","slug":"tele2play","themedLogo":{"light":{"url":"https://new.static.tv.nu/158747195","isFallback":false},"dark":{"url":"https://new.static.tv.nu/158747194","isFallback":false}},"url":"https://www.comhemplay.se/open/vod/SH016259780000?utm_source=tv.nu&utm_medium=partner&utm_campaign=tabla&utm_content=CSI%3A+Crime+Scene+Investigation"},{"name":"Prime Video","slug":"prime-video","themedLogo":{"light":{"url":"https://new.static.tv.nu/23085972","isFallback":false},"dark":{"url":"https://new.static.tv.nu/275111","isFallback":true}},"url":"https://app.primevideo.com/detail?gti=amzn1.dv.gti.54af67f9-e58f-e6db-4991-81eb4f2efa37&utm_source=tv.nu"}],"broadcast":{"id":"1OoSZY-7Q7-1DzQ","startTime":1660878900000,"endTime":1660881600000,"channel":{"name":"RTL","slug":"rtl","themedLogo":{"light":{"url":"https://new.static.tv.nu/19402170","isFallback":false},"dark":{"url":"https://new.static.tv.nu/59995595","isFallback":true}}}},"totalEpisodes":24,"episodeNumber":19,"seasonNumber":6}]}}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-08-19T03:15:00.000Z', + stop: '2022-08-19T04:00:00.000Z', + title: 'CSI: Den Tätern auf der Spur', + description: + 'Hellseherin Sedona Wiley wird tot aufgefunden. Die Ermittlungen führen zu einem alten Mord. Gordon Wallace wurde vor 15 Jahren beschuldigt, seine Frau getötet zu haben, jedoch wurde nie eine Leiche gefunden.', + icon: 'https://new.static.tv.nu/13119997', + category: ['Action', 'Kriminaldrama', 'Mysterium', 'Spänning', 'Thriller'], + season: 6, + episode: 19 + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"meta":{"status":200},"data":{"broadcasts":[]}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.post.lu/tv.post.lu.config.js b/sites/tv.post.lu/tv.post.lu.config.js index cc223270..74162f9a 100644 --- a/sites/tv.post.lu/tv.post.lu.config.js +++ b/sites/tv.post.lu/tv.post.lu.config.js @@ -1,56 +1,56 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.post.lu', - days: 2, - url({ channel, date }) { - return `https://tv.post.lu/api/channels?id=${channel.site_id}&date=${date.format('YYYY-MM-DD')}` - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - category: item.program_type, - icon: item.image_url, - start: dayjs.unix(item.tsStart), - stop: dayjs.unix(item.tsEnd) - }) - }) - - return programs - }, - async channels() { - const promises = [...Array(17).keys()].map(i => - axios.get(`https://tv.post.lu/api/channels/?page=${i + 1}`) - ) - - const channels = [] - await Promise.all(promises).then(values => { - values.forEach(r => { - let items = r.data.result.data - items.forEach(item => { - channels.push({ - lang: item.language.code, - name: item.name, - site_id: item.id - }) - }) - }) - }) - - return channels - } -} - -function parseItems(content) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !data.result || !data.result.epg || !Array.isArray(data.result.epg.programme)) - return [] - - return data.result.epg.programme -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.post.lu', + days: 2, + url({ channel, date }) { + return `https://tv.post.lu/api/channels?id=${channel.site_id}&date=${date.format('YYYY-MM-DD')}` + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + category: item.program_type, + icon: item.image_url, + start: dayjs.unix(item.tsStart), + stop: dayjs.unix(item.tsEnd) + }) + }) + + return programs + }, + async channels() { + const promises = [...Array(17).keys()].map(i => + axios.get(`https://tv.post.lu/api/channels/?page=${i + 1}`) + ) + + const channels = [] + await Promise.all(promises).then(values => { + values.forEach(r => { + let items = r.data.result.data + items.forEach(item => { + channels.push({ + lang: item.language.code, + name: item.name, + site_id: item.id + }) + }) + }) + }) + + return channels + } +} + +function parseItems(content) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !data.result || !data.result.epg || !Array.isArray(data.result.epg.programme)) + return [] + + return data.result.epg.programme +} diff --git a/sites/tv.post.lu/tv.post.lu.test.js b/sites/tv.post.lu/tv.post.lu.test.js index 49aedbb4..877745b4 100644 --- a/sites/tv.post.lu/tv.post.lu.test.js +++ b/sites/tv.post.lu/tv.post.lu.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/tv.post.lu/tv.post.lu.config.js --output=./sites/tv.post.lu/tv.post.lu.channels.xml -// npm run grab -- --site=tv.post.lu - -const { parser, url } = require('./tv.post.lu.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '269695d0-8076-11e9-b5ca-f345a2ed0fbe', - xmltv_id: 'DasErste.de' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.post.lu/api/channels?id=269695d0-8076-11e9-b5ca-f345a2ed0fbe&date=2023-01-16' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Tagesschau', - description: - 'Das Flaggschiff unter den deutschen Nachrichtensendungen ist gleichzeitig die "dienstälteste" noch bestehende Sendung im deutschen Fernsehen. In bis zu 20 am Tag produzierten Sendungen wird die Komplexität des Weltgeschehens verständlich erklärt und in komprimierter Form über aktuelle politische, wirtschaftliche, soziale, kulturelle, sportliche und sonstige Ereignisse berichtet.', - category: 'Nachrichten', - icon: 'https://mp-photos-cdn.azureedge.net/container3cc71e4948ac40ab803c26e0abc2e3e5/original/e6eb49013a822f5c6eb2e7701e69a1f80aa0b947.jpg', - start: '2023-01-16T00:05:00.000Z', - stop: '2023-01-16T00:10:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '' }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.post.lu/tv.post.lu.config.js --output=./sites/tv.post.lu/tv.post.lu.channels.xml +// npm run grab -- --site=tv.post.lu + +const { parser, url } = require('./tv.post.lu.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-16', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '269695d0-8076-11e9-b5ca-f345a2ed0fbe', + xmltv_id: 'DasErste.de' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.post.lu/api/channels?id=269695d0-8076-11e9-b5ca-f345a2ed0fbe&date=2023-01-16' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Tagesschau', + description: + 'Das Flaggschiff unter den deutschen Nachrichtensendungen ist gleichzeitig die "dienstälteste" noch bestehende Sendung im deutschen Fernsehen. In bis zu 20 am Tag produzierten Sendungen wird die Komplexität des Weltgeschehens verständlich erklärt und in komprimierter Form über aktuelle politische, wirtschaftliche, soziale, kulturelle, sportliche und sonstige Ereignisse berichtet.', + category: 'Nachrichten', + icon: 'https://mp-photos-cdn.azureedge.net/container3cc71e4948ac40ab803c26e0abc2e3e5/original/e6eb49013a822f5c6eb2e7701e69a1f80aa0b947.jpg', + start: '2023-01-16T00:05:00.000Z', + stop: '2023-01-16T00:10:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '' }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tv.trueid.net/tv.trueid.net.config.js b/sites/tv.trueid.net/tv.trueid.net.config.js index a1dbbb5d..a8adb28f 100644 --- a/sites/tv.trueid.net/tv.trueid.net.config.js +++ b/sites/tv.trueid.net/tv.trueid.net.config.js @@ -1,66 +1,66 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - delay: 5000, - site: 'tv.trueid.net', - days: 2, - url: function ({ channel, date }) { - return `https://tv.trueid.net/tvguide/all/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - request: { - jar: null - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.detail ? item.detail.thumb : null -} - -function parseStart(item) { - return item.detail ? dayjs.utc(item.detail.start_date) : null -} - -function parseStop(item) { - return item.detail ? dayjs.utc(item.detail.end_date) : null -} - -function parseContent(content, channel) { - const $ = cheerio.load(content) - const nextData = $('#__NEXT_DATA__').html() - const data = JSON.parse(nextData) - if ( - !data || - !data.props || - !data.props.pageProps || - !data.props.pageProps.listEPG || - !Array.isArray(data.props.pageProps.listEPG.data) - ) - return null - - return data.props.pageProps.listEPG.data.find(ch => ch.slug === channel.site_id) -} - -function parseItems(content, channel) { - const data = parseContent(content, channel) - if (!data || !Array.isArray(data.programList)) return [] - - return data.programList -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + delay: 5000, + site: 'tv.trueid.net', + days: 2, + url: function ({ channel, date }) { + return `https://tv.trueid.net/tvguide/all/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + request: { + jar: null + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.detail ? item.detail.thumb : null +} + +function parseStart(item) { + return item.detail ? dayjs.utc(item.detail.start_date) : null +} + +function parseStop(item) { + return item.detail ? dayjs.utc(item.detail.end_date) : null +} + +function parseContent(content, channel) { + const $ = cheerio.load(content) + const nextData = $('#__NEXT_DATA__').html() + const data = JSON.parse(nextData) + if ( + !data || + !data.props || + !data.props.pageProps || + !data.props.pageProps.listEPG || + !Array.isArray(data.props.pageProps.listEPG.data) + ) + return null + + return data.props.pageProps.listEPG.data.find(ch => ch.slug === channel.site_id) +} + +function parseItems(content, channel) { + const data = parseContent(content, channel) + if (!data || !Array.isArray(data.programList)) return [] + + return data.programList +} diff --git a/sites/tv.trueid.net/tv.trueid.net.test.js b/sites/tv.trueid.net/tv.trueid.net.test.js index 77124357..ebed99d7 100644 --- a/sites/tv.trueid.net/tv.trueid.net.test.js +++ b/sites/tv.trueid.net/tv.trueid.net.test.js @@ -1,43 +1,43 @@ -// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/tv.trueid.net/tv.trueid.net.config.js --channels=sites/tv.trueid.net/tv.trueid.net.channels.xml --output=guide.xml --timeout=30000 --days=2 - -const { parser, url } = require('./tv.trueid.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tv-nfl-nba', - xmltv_id: 'NFLNBATV.us', - name: 'NFL & NBA TV' -} -const content = - '' - -it('can generate valid url', () => { - const result = url({ channel, date }) - expect(result).toBe('https://tv.trueid.net/tvguide/all/tv-nfl-nba/2021-10-28') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-10-28T19:00:00.000Z', - stop: '2021-10-28T21:00:00.000Z', - title: 'NBA 2021/22', - icon: 'https://epgthumb.dmpcdn.com/thumbnail_large/t513/20211029/20211029_020000.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ date, channel, content: '{}' }) - expect(result).toMatchObject([]) -}) +// NODE_OPTIONS=--insecure-http-parser npx epg-grabber --config=sites/tv.trueid.net/tv.trueid.net.config.js --channels=sites/tv.trueid.net/tv.trueid.net.channels.xml --output=guide.xml --timeout=30000 --days=2 + +const { parser, url } = require('./tv.trueid.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tv-nfl-nba', + xmltv_id: 'NFLNBATV.us', + name: 'NFL & NBA TV' +} +const content = + '' + +it('can generate valid url', () => { + const result = url({ channel, date }) + expect(result).toBe('https://tv.trueid.net/tvguide/all/tv-nfl-nba/2021-10-28') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-10-28T19:00:00.000Z', + stop: '2021-10-28T21:00:00.000Z', + title: 'NBA 2021/22', + icon: 'https://epgthumb.dmpcdn.com/thumbnail_large/t513/20211029/20211029_020000.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ date, channel, content: '{}' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.vera.com.uy/tv.vera.com.uy.config.js b/sites/tv.vera.com.uy/tv.vera.com.uy.config.js index 2f58fe9b..c321be38 100644 --- a/sites/tv.vera.com.uy/tv.vera.com.uy.config.js +++ b/sites/tv.vera.com.uy/tv.vera.com.uy.config.js @@ -1,108 +1,108 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos' - -module.exports = { - site: 'tv.vera.com.uy', - days: 2, - async url({ date, channel }) { - const session = await loadSessionDetails() - if (!session || !session.token) return null - - return `${API_ENDPOINT}/canales/epg/${ - channel.site_id - }?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}` - }, - request: { - async headers() { - const session = await loadSessionDetails() - if (!session || !session.jwt) return null - - return { - authorization: `Bearer ${session.jwt}`, - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - } - } - }, - parser({ content }) { - let programs = [] - let items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.nombre_programa, - sub_title: item.subtitle, - description: item.descripcion_programa, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const session = await loadSessionDetails() - if (!session || !session.jwt || !session.token) return null - - const data = await axios - .get(`${API_ENDPOINT}/listas/68?token=${session.token}`, { - headers: { - authorization: `Bearer ${session.jwt}`, - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - } - }) - .then(r => r.data) - .catch(console.error) - - return data.contenidos.map(c => { - return { - lang: 'es', - site_id: c.public_id, - name: c.nombre - } - }) - } -} - -function parseStart(item) { - return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseStop(item) { - return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data)) return [] - - return data.data -} - -function loadSessionDetails() { - return axios - .post( - 'https://veratv-be.vera.com.uy/api/sesiones', - { - tipo: 'anonima' - }, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - .then(r => r.data) - .catch(console.log) -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +const API_ENDPOINT = 'https://cds-frontend.vera.com.uy/api-contenidos' + +module.exports = { + site: 'tv.vera.com.uy', + days: 2, + async url({ date, channel }) { + const session = await loadSessionDetails() + if (!session || !session.token) return null + + return `${API_ENDPOINT}/canales/epg/${ + channel.site_id + }?limit=500&dias_siguientes=0&fecha=${date.format('YYYY-MM-DD')}&token=${session.token}` + }, + request: { + async headers() { + const session = await loadSessionDetails() + if (!session || !session.jwt) return null + + return { + authorization: `Bearer ${session.jwt}`, + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + } + } + }, + parser({ content }) { + let programs = [] + let items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.nombre_programa, + sub_title: item.subtitle, + description: item.descripcion_programa, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const session = await loadSessionDetails() + if (!session || !session.jwt || !session.token) return null + + const data = await axios + .get(`${API_ENDPOINT}/listas/68?token=${session.token}`, { + headers: { + authorization: `Bearer ${session.jwt}`, + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + } + }) + .then(r => r.data) + .catch(console.error) + + return data.contenidos.map(c => { + return { + lang: 'es', + site_id: c.public_id, + name: c.nombre + } + }) + } +} + +function parseStart(item) { + return dayjs.tz(item.fecha_hora_inicio, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseStop(item) { + return dayjs.tz(item.fecha_hora_fin, 'YYYY-MM-DD HH:mm:ss', 'America/Montevideo') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data)) return [] + + return data.data +} + +function loadSessionDetails() { + return axios + .post( + 'https://veratv-be.vera.com.uy/api/sesiones', + { + tipo: 'anonima' + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .then(r => r.data) + .catch(console.log) +} diff --git a/sites/tv.vera.com.uy/tv.vera.com.uy.test.js b/sites/tv.vera.com.uy/tv.vera.com.uy.test.js index 9cdfb281..075f1ea9 100644 --- a/sites/tv.vera.com.uy/tv.vera.com.uy.test.js +++ b/sites/tv.vera.com.uy/tv.vera.com.uy.test.js @@ -1,88 +1,88 @@ -// npm run channels:parse -- --config=./sites/tv.vera.com.uy/tv.vera.com.uy.config.js --output=./sites/tv.vera.com.uy/tv.vera.com.uy.channels.xml -// npm run grab -- --site=tv.vera.com.uy - -const { parser, url, request } = require('./tv.vera.com.uy.config.js') -const fs = require('fs') -const axios = require('axios') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -axios.post.mockImplementation((url, data, opts) => { - if ( - url === 'https://veratv-be.vera.com.uy/api/sesiones' && - JSON.stringify(opts.headers) === - JSON.stringify({ - 'Content-Type': 'application/json' - }) && - JSON.stringify(data) === - JSON.stringify({ - tipo: 'anonima' - }) - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) - }) - } else { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) - }) - } -}) - -const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2s6nd', - xmltv_id: 'Canal5.uy' -} - -it('can generate valid url', async () => { - const result = await url({ date, channel }) - - expect(result).toBe( - 'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B' - ) -}) - -it('can generate valid request headers', async () => { - const result = await request.headers() - - expect(result).toMatchObject({ - authorization: - 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s', - 'x-frontend-id': 1196, - 'x-service-id': 3, - 'x-system-id': 1 - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-02-11T02:30:00.000Z', - stop: '2023-02-11T04:00:00.000Z', - title: 'Canal 5 Noticias rep.', - sub_title: '', - description: '' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.vera.com.uy/tv.vera.com.uy.config.js --output=./sites/tv.vera.com.uy/tv.vera.com.uy.channels.xml +// npm run grab -- --site=tv.vera.com.uy + +const { parser, url, request } = require('./tv.vera.com.uy.config.js') +const fs = require('fs') +const axios = require('axios') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +axios.post.mockImplementation((url, data, opts) => { + if ( + url === 'https://veratv-be.vera.com.uy/api/sesiones' && + JSON.stringify(opts.headers) === + JSON.stringify({ + 'Content-Type': 'application/json' + }) && + JSON.stringify(data) === + JSON.stringify({ + tipo: 'anonima' + }) + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/session.json'))) + }) + } else { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/no_session.json'))) + }) + } +}) + +const date = dayjs.utc('2023-02-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2s6nd', + xmltv_id: 'Canal5.uy' +} + +it('can generate valid url', async () => { + const result = await url({ date, channel }) + + expect(result).toBe( + 'https://cds-frontend.vera.com.uy/api-contenidos/canales/epg/2s6nd?limit=500&dias_siguientes=0&fecha=2023-02-11&token=MpDY52p1V6g511VSABp1015B' + ) +}) + +it('can generate valid request headers', async () => { + const result = await request.headers() + + expect(result).toMatchObject({ + authorization: + 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOnsidGlwbyI6ImFub25pbWEifSwic3ViIjoiTXBEWTUycDFWNmc1MTFWU0FCcDEwMTVCIiwicHJuIjp7ImlkX3NlcnZpY2lvIjozLCJpZF9mcm9udGVuZCI6MTE5NiwiaXAiOiIxNzkuMjcuMTU0LjI0MiIsImlwX3JlZmVyZW5jaWFkYSI6IjE4OC4yNDIuNDguOTMiLCJpZF9kaXNwb3NpdGl2byI6MH0sImF1ZCI6IkFwcHNcL1dlYnMgRnJvbnRlbmRzIiwiaWF0IjoxNjc1ODI3NDU2LCJleHAiOjE2NzU4NDkwNTZ9.8bAQciQl5DOIZF7GgCl6ad-KJUSpqQREetozGv_IH5s', + 'x-frontend-id': 1196, + 'x-service-id': 3, + 'x-system-id': 1 + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-02-11T02:30:00.000Z', + stop: '2023-02-11T04:00:00.000Z', + title: 'Canal 5 Noticias rep.', + sub_title: '', + description: '' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/tv.yandex.ru/tv.yandex.ru.config.js b/sites/tv.yandex.ru/tv.yandex.ru.config.js index c5883e4c..ac42f0cd 100644 --- a/sites/tv.yandex.ru/tv.yandex.ru.config.js +++ b/sites/tv.yandex.ru/tv.yandex.ru.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.yandex.ru', - days: 2, - url: function ({ date, channel }) { - const [region, id] = channel.site_id.split('#') - - return `https://tv.yandex.ru/${region}/channel/${id}?date=${date.format('YYYY-MM-DD')}` - }, - request: { - headers: { - Cookie: - 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' - } - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.program.description, - category: item.program.type.name, - start: dayjs(item.start), - stop: dayjs(item.finish) - }) - }) - - return programs - } -} - -function parseContent(content) { - const [, initialState] = content.match(/window.__INITIAL_STATE__ = (.*);/i) || [null, null] - if (!initialState) return null - const data = JSON.parse(initialState) - if (!data) return null - - return data.channel -} - -function parseItems(content) { - const data = parseContent(content) - if (!data || !data.schedule || !Array.isArray(data.schedule.events)) return [] - - return data.schedule.events -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.yandex.ru', + days: 2, + url: function ({ date, channel }) { + const [region, id] = channel.site_id.split('#') + + return `https://tv.yandex.ru/${region}/channel/${id}?date=${date.format('YYYY-MM-DD')}` + }, + request: { + headers: { + Cookie: + 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' + } + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.program.description, + category: item.program.type.name, + start: dayjs(item.start), + stop: dayjs(item.finish) + }) + }) + + return programs + } +} + +function parseContent(content) { + const [, initialState] = content.match(/window.__INITIAL_STATE__ = (.*);/i) || [null, null] + if (!initialState) return null + const data = JSON.parse(initialState) + if (!data) return null + + return data.channel +} + +function parseItems(content) { + const data = parseContent(content) + if (!data || !data.schedule || !Array.isArray(data.schedule.events)) return [] + + return data.schedule.events +} diff --git a/sites/tv.yandex.ru/tv.yandex.ru.test.js b/sites/tv.yandex.ru/tv.yandex.ru.test.js index 74112b07..d1a65bc0 100644 --- a/sites/tv.yandex.ru/tv.yandex.ru.test.js +++ b/sites/tv.yandex.ru/tv.yandex.ru.test.js @@ -1,56 +1,56 @@ -// npm run grab -- --site=tv.yandex.ru - -const { parser, url, request } = require('./tv.yandex.ru.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '162#31-kanal-429', - xmltv_id: '31Kanal.kz' -} -const content = - ' ' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tv.yandex.ru/162/channel/31-kanal-429?date=2021-11-25' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Cookie: - 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' - }) -}) - -it('can parse response', () => { - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-24T23:58:00.000Z', - title: 'Ризамын (каз.).', - category: 'досуг', - description: 'kLX6FVKAIiDCGBFE' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tv.yandex.ru + +const { parser, url, request } = require('./tv.yandex.ru.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '162#31-kanal-429', + xmltv_id: '31Kanal.kz' +} +const content = + ' ' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tv.yandex.ru/162/channel/31-kanal-429?date=2021-11-25' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Cookie: + 'yandexuid=8747786251615498142; Expires=Tue, 11 Mar 2031 21:29:02 GMT; Domain=yandex.ru; Path=/' + }) +}) + +it('can parse response', () => { + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-24T23:58:00.000Z', + title: 'Ризамын (каз.).', + category: 'досуг', + description: 'kLX6FVKAIiDCGBFE' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv.yettel.hu/tv.yettel.hu.config.js b/sites/tv.yettel.hu/tv.yettel.hu.config.js index 9e54b779..95760b90 100644 --- a/sites/tv.yettel.hu/tv.yettel.hu.config.js +++ b/sites/tv.yettel.hu/tv.yettel.hu.config.js @@ -1,69 +1,69 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tv.yettel.hu', - days: 2, - url: function ({ channel, date }) { - return `https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=${ - channel.site_id - }&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=${date.unix()}&endTime=${date - .add(1, 'd') - .unix()}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.Title, - description: item.ShortDescription, - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://dev.mytvback.com/api/19/default/hu-HU/content/CHA_LIVE_MYTV2_HU/children') - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let item of data.Content.List) { - channels.push({ - lang: 'hu', - site_id: item.Pid, - name: item.CallLetter - }) - } - - return channels - } -} - -function parseIcon(item) { - if (Array.isArray(item.Images.Cover) && item.Images.Cover.length) { - return item.Images.Cover[0].Url - } - - return null -} - -function parseStart(item) { - return dayjs.unix(item.Start) -} - -function parseStop(item) { - return dayjs.unix(item.End) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.Content)) return [] - - return data.Content -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tv.yettel.hu', + days: 2, + url: function ({ channel, date }) { + return `https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=${ + channel.site_id + }&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=${date.unix()}&endTime=${date + .add(1, 'd') + .unix()}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.Title, + description: item.ShortDescription, + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://dev.mytvback.com/api/19/default/hu-HU/content/CHA_LIVE_MYTV2_HU/children') + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let item of data.Content.List) { + channels.push({ + lang: 'hu', + site_id: item.Pid, + name: item.CallLetter + }) + } + + return channels + } +} + +function parseIcon(item) { + if (Array.isArray(item.Images.Cover) && item.Images.Cover.length) { + return item.Images.Cover[0].Url + } + + return null +} + +function parseStart(item) { + return dayjs.unix(item.Start) +} + +function parseStop(item) { + return dayjs.unix(item.End) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.Content)) return [] + + return data.Content +} diff --git a/sites/tv.yettel.hu/tv.yettel.hu.test.js b/sites/tv.yettel.hu/tv.yettel.hu.test.js index f283d71c..2cccbb7a 100644 --- a/sites/tv.yettel.hu/tv.yettel.hu.test.js +++ b/sites/tv.yettel.hu/tv.yettel.hu.test.js @@ -1,83 +1,83 @@ -// npm run channels:parse -- --config=./sites/tv.yettel.hu/tv.yettel.hu.config.js --output=./sites/tv.yettel.hu/tv.yettel.hu.channels.xml -// npm run grab -- --site=tv.yettel.hu - -const { parser, url } = require('./tv.yettel.hu.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-06-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'LCH1', - xmltv_id: 'M1.hu' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=LCH1&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=1655424000&endTime=1655510400' - ) -}) - -it('can parse response', () => { - const content = `{ - "Content": [ - { - "AgeRatingPid": "", - "catchup_days": "0", - "AvailableUntil": 1655445600, - "Description": "", - "End": 1655445600, - "LiveChannelPid": "LCH1", - "ch_id": "1", - "LiveProgramPid": "LEP3906574", - "pr_id": "3906574", - "se_id": "13986", - "LiveSeriesPid": "LSE13986", - "Pid": "LSC17202373", - "id": "17202373", - "Rating": 0, - "RatingTotalVotes": 0, - "ShortDescription": "A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.", - "Start": 1655443980, - "Title": "Ma reggel", - "Year": 2022, - "GenrePids": [ - "GEN184" - ], - "ge_id": "184", - "IsCatchup": "1", - "ChannelIsCatchup": "0", - "Images": { - "Cover": [ - { - "Url": "https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg" - } - ] - } - }]}` - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-06-17T05:33:00.000Z', - stop: '2022-06-17T06:00:00.000Z', - title: 'Ma reggel', - description: - 'A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.', - icon: 'https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"Content":[],"HttpStatusCode":200,"StatusCode":0,"StatusMessage":"OK","Severity":1}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv.yettel.hu/tv.yettel.hu.config.js --output=./sites/tv.yettel.hu/tv.yettel.hu.channels.xml +// npm run grab -- --site=tv.yettel.hu + +const { parser, url } = require('./tv.yettel.hu.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-06-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'LCH1', + xmltv_id: 'M1.hu' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://dev.mytvback.com/api/19/default/hu-HU/schedules?livechannelpids=LCH1&includeImages=cover%3A100%3A144&filterAvailability=false&startTime=1655424000&endTime=1655510400' + ) +}) + +it('can parse response', () => { + const content = `{ + "Content": [ + { + "AgeRatingPid": "", + "catchup_days": "0", + "AvailableUntil": 1655445600, + "Description": "", + "End": 1655445600, + "LiveChannelPid": "LCH1", + "ch_id": "1", + "LiveProgramPid": "LEP3906574", + "pr_id": "3906574", + "se_id": "13986", + "LiveSeriesPid": "LSE13986", + "Pid": "LSC17202373", + "id": "17202373", + "Rating": 0, + "RatingTotalVotes": 0, + "ShortDescription": "A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.", + "Start": 1655443980, + "Title": "Ma reggel", + "Year": 2022, + "GenrePids": [ + "GEN184" + ], + "ge_id": "184", + "IsCatchup": "1", + "ChannelIsCatchup": "0", + "Images": { + "Cover": [ + { + "Url": "https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg" + } + ] + } + }]}` + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-06-17T05:33:00.000Z', + stop: '2022-06-17T06:00:00.000Z', + title: 'Ma reggel', + description: + 'A Ma reggel az MTVA saját gyártású, minden hétköznap jelentkező reggeli politikai és közéleti témákkal foglalkozó műsora.', + icon: 'https://static.mytvback.com/userfiles/c/0/c01d48a36b913a7afb0dcb5edba33849_thum_100x144.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"Content":[],"HttpStatusCode":200,"StatusCode":0,"StatusMessage":"OK","Severity":1}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv24.co.uk/tv24.co.uk.config.js b/sites/tv24.co.uk/tv24.co.uk.config.js index 6a9a7ff9..72755577 100644 --- a/sites/tv24.co.uk/tv24.co.uk.config.js +++ b/sites/tv24.co.uk/tv24.co.uk.config.js @@ -1,92 +1,92 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tv24.co.uk', - days: 2, - url: function ({ channel, date }) { - return `https://tv24.co.uk/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let html = await axios - .get('https://tv24.co.uk/x/settings/addremove') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const nums = $('li') - .toArray() - .map(item => $(item).data('channel')) - html = await axios - .get('https://tv24.co.uk', { - headers: { - Cookie: `selectedChannels=${nums.join(',')}` - } - }) - .then(r => r.data) - .catch(console.log) - $ = cheerio.load(html) - const items = $('li.c').toArray() - - return items.map(item => { - const name = $(item).find('h3').text().trim() - const link = $(item).find('.channel').attr('href') - const [, site_id] = link.match(/\/channel\/(.*)/) || [null, null] - - return { - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('h3').text() -} - -function parseDescription($item) { - return $item('p').text() -} - -function parseStart($item, date) { - const time = $item('.time').text() - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h:mma') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.program').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tv24.co.uk', + days: 2, + url: function ({ channel, date }) { + return `https://tv24.co.uk/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let html = await axios + .get('https://tv24.co.uk/x/settings/addremove') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const nums = $('li') + .toArray() + .map(item => $(item).data('channel')) + html = await axios + .get('https://tv24.co.uk', { + headers: { + Cookie: `selectedChannels=${nums.join(',')}` + } + }) + .then(r => r.data) + .catch(console.log) + $ = cheerio.load(html) + const items = $('li.c').toArray() + + return items.map(item => { + const name = $(item).find('h3').text().trim() + const link = $(item).find('.channel').attr('href') + const [, site_id] = link.match(/\/channel\/(.*)/) || [null, null] + + return { + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('h3').text() +} + +function parseDescription($item) { + return $item('p').text() +} + +function parseStart($item, date) { + const time = $item('.time').text() + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h:mma') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.program').toArray() +} diff --git a/sites/tv24.co.uk/tv24.co.uk.test.js b/sites/tv24.co.uk/tv24.co.uk.test.js index 3a569ab2..95bc72e3 100644 --- a/sites/tv24.co.uk/tv24.co.uk.test.js +++ b/sites/tv24.co.uk/tv24.co.uk.test.js @@ -1,53 +1,53 @@ -// npm run channels:parse -- --config=./sites/tv24.co.uk/tv24.co.uk.config.js --output=./sites/tv24.co.uk/tv24.co.uk.channels.xml -// npm run grab -- --site=tv24.co.uk - -const { parser, url } = require('./tv24.co.uk.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'bbc-two', - xmltv_id: 'BBCTwo.uk' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv24.co.uk/x/channel/bbc-two/0/2022-08-28') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-28T05:05:00.000Z', - stop: '2022-08-28T06:05:00.000Z', - title: "Gardeners' World", - description: - 'Arit Anderson discovers a paradise garden in Cambridge which has become a focal point for the local community, and Frances Tophill shares the joy of collecting and saving heirloom vegetable seeds on a visit to Pembrokeshire.' - }) - - expect(results[22]).toMatchObject({ - start: '2022-08-29T05:30:00.000Z', - stop: '2022-08-29T06:00:00.000Z', - title: 'Animal Park', - description: - "One of the park's vultures has laid an egg. It is ten years since Longleat had a successfully reared vulture chick, so the keepers send Hamza to find out if the parents are incubating their egg." - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv24.co.uk/tv24.co.uk.config.js --output=./sites/tv24.co.uk/tv24.co.uk.channels.xml +// npm run grab -- --site=tv24.co.uk + +const { parser, url } = require('./tv24.co.uk.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-08-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'bbc-two', + xmltv_id: 'BBCTwo.uk' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv24.co.uk/x/channel/bbc-two/0/2022-08-28') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-28T05:05:00.000Z', + stop: '2022-08-28T06:05:00.000Z', + title: "Gardeners' World", + description: + 'Arit Anderson discovers a paradise garden in Cambridge which has become a focal point for the local community, and Frances Tophill shares the joy of collecting and saving heirloom vegetable seeds on a visit to Pembrokeshire.' + }) + + expect(results[22]).toMatchObject({ + start: '2022-08-29T05:30:00.000Z', + stop: '2022-08-29T06:00:00.000Z', + title: 'Animal Park', + description: + "One of the park's vultures has laid an egg. It is ten years since Longleat had a successfully reared vulture chick, so the keepers send Hamza to find out if the parents are incubating their egg." + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv24.se/tv24.se.config.js b/sites/tv24.se/tv24.se.config.js index 1e981cc2..832d6ec0 100644 --- a/sites/tv24.se/tv24.se.config.js +++ b/sites/tv24.se/tv24.se.config.js @@ -1,163 +1,163 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tv24.se', - days: 2, - url: function ({ channel, date }) { - return `https://tv24.se/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` - }, - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - const details = await loadProgramDetails($item) - programs.push({ - title: parseTitle($item), - description: details.description, - actors: details.actors, - icon: details.icon, - category: details.category, - sub_title: details.sub_title, - season: details.season, - episode: details.episode, - start, - stop - }) - } - - return programs - }, - async channels() { - let html = await axios - .get('https://tv24.se/x/settings/addremove') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - const nums = $('li') - .toArray() - .map(item => $(item).data('channel')) - html = await axios - .get('https://tv24.se', { - headers: { - Cookie: `selectedChannels=${nums.join(',')}` - } - }) - .then(r => r.data) - .catch(console.log) - $ = cheerio.load(html) - const items = $('li.c').toArray() - - return items.map(item => { - const name = $(item).find('h3').text().trim() - const link = $(item).find('.channel').attr('href') - const [, site_id] = link.match(/\/kanal\/(.*)/) || [null, null] - - return { - lang: 'sv', - site_id, - name - } - }) - } -} - -async function loadProgramDetails($item) { - const programId = $item('a').attr('href') - const data = await axios - .get(`https://tv24.se/x${programId}/0/0`) - .then(r => r.data) - .catch(console.error) - if (!data) return Promise.resolve({}) - const $ = cheerio.load(data.contentBefore + data.contentAfter) - - return Promise.resolve({ - icon: parseIcon($), - actors: parseActors($), - description: parseDescription($), - category: parseCategory($), - sub_title: parseSubTitle($), - season: parseSeason($), - episode: parseEpisode($) - }) -} - -function parseIcon($) { - const style = $('.image > .actual').attr('style') - const [, url] = style.match(/background-image: url\('([^']+)'\)/) - - return url -} - -function parseSeason($) { - const [, season] = $('.sub-title') - .text() - .trim() - .match(/Säsong (\d+)/) || [null, ''] - - return parseInt(season) -} - -function parseEpisode($) { - const [, episode] = $('.sub-title') - .text() - .trim() - .match(/Avsnitt (\d+)/) || [null, ''] - - return parseInt(episode) -} - -function parseSubTitle($) { - const [, subtitle] = $('.sub-title').text().trim().split(': ') - - return subtitle -} - -function parseCategory($) { - return $('.extras > dt:contains(Kategori)').next().text().trim().split(' / ') -} - -function parseActors($) { - return $('.cast > li') - .map((i, el) => { - return $(el).find('.name').text().trim() - }) - .get() -} - -function parseDescription($) { - return $('.info > p').text().trim() -} - -function parseTitle($item) { - return $item('h3').text() -} - -function parseStart($item, date) { - const time = $item('.time') - - return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.program').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tv24.se', + days: 2, + url: function ({ channel, date }) { + return `https://tv24.se/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}` + }, + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + const details = await loadProgramDetails($item) + programs.push({ + title: parseTitle($item), + description: details.description, + actors: details.actors, + icon: details.icon, + category: details.category, + sub_title: details.sub_title, + season: details.season, + episode: details.episode, + start, + stop + }) + } + + return programs + }, + async channels() { + let html = await axios + .get('https://tv24.se/x/settings/addremove') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + const nums = $('li') + .toArray() + .map(item => $(item).data('channel')) + html = await axios + .get('https://tv24.se', { + headers: { + Cookie: `selectedChannels=${nums.join(',')}` + } + }) + .then(r => r.data) + .catch(console.log) + $ = cheerio.load(html) + const items = $('li.c').toArray() + + return items.map(item => { + const name = $(item).find('h3').text().trim() + const link = $(item).find('.channel').attr('href') + const [, site_id] = link.match(/\/kanal\/(.*)/) || [null, null] + + return { + lang: 'sv', + site_id, + name + } + }) + } +} + +async function loadProgramDetails($item) { + const programId = $item('a').attr('href') + const data = await axios + .get(`https://tv24.se/x${programId}/0/0`) + .then(r => r.data) + .catch(console.error) + if (!data) return Promise.resolve({}) + const $ = cheerio.load(data.contentBefore + data.contentAfter) + + return Promise.resolve({ + icon: parseIcon($), + actors: parseActors($), + description: parseDescription($), + category: parseCategory($), + sub_title: parseSubTitle($), + season: parseSeason($), + episode: parseEpisode($) + }) +} + +function parseIcon($) { + const style = $('.image > .actual').attr('style') + const [, url] = style.match(/background-image: url\('([^']+)'\)/) + + return url +} + +function parseSeason($) { + const [, season] = $('.sub-title') + .text() + .trim() + .match(/Säsong (\d+)/) || [null, ''] + + return parseInt(season) +} + +function parseEpisode($) { + const [, episode] = $('.sub-title') + .text() + .trim() + .match(/Avsnitt (\d+)/) || [null, ''] + + return parseInt(episode) +} + +function parseSubTitle($) { + const [, subtitle] = $('.sub-title').text().trim().split(': ') + + return subtitle +} + +function parseCategory($) { + return $('.extras > dt:contains(Kategori)').next().text().trim().split(' / ') +} + +function parseActors($) { + return $('.cast > li') + .map((i, el) => { + return $(el).find('.name').text().trim() + }) + .get() +} + +function parseDescription($) { + return $('.info > p').text().trim() +} + +function parseTitle($item) { + return $item('h3').text() +} + +function parseStart($item, date) { + const time = $item('.time') + + return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.program').toArray() +} diff --git a/sites/tv24.se/tv24.se.test.js b/sites/tv24.se/tv24.se.test.js index 8cd14fac..24b6726e 100644 --- a/sites/tv24.se/tv24.se.test.js +++ b/sites/tv24.se/tv24.se.test.js @@ -1,78 +1,78 @@ -// npm run channels:parse -- --config=./sites/tv24.se/tv24.se.config.js --output=./sites/tv24.se/tv24.se.channels.xml -// npm run grab -- --site=tv24.se - -const { parser, url } = require('./tv24.se.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-08-26', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'svt1', - xmltv_id: 'SVT1.se' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tv24.se/x/channel/svt1/0/2022-08-26') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - axios.get.mockImplementation(url => { - if (url === 'https://tv24.se/x/b/rh7f40-1hkm/0/0') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) - }) - } else if (url === 'https://tv24.se/x/b/rh9dhc-1hkm/0/0') { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-08-26T04:00:00.000Z', - stop: '2022-08-26T07:10:00.000Z', - title: 'Morgonstudion', - icon: 'https://jrsy.tmsimg.com/assets/p14436175_i_h9_ad.jpg', - description: - 'Dagens viktigaste nyheter och analyser med ständiga uppdateringar. Vi sänder direkt inrikes- och utrikesnyheter inklusive sport, kultur och nöje. Dessutom intervjuer med aktuella gäster. Nyhetssammanfattningar varje kvart med start kl 06.00.', - actors: ['Carolina Neurath', 'Karin Magnusson', 'Pelle Nilsson', 'Ted Wigren'] - }) - - expect(results[33]).toMatchObject({ - start: '2022-08-27T05:20:00.000Z', - stop: '2022-08-27T05:50:00.000Z', - title: 'Uppdrag granskning', - icon: 'https://jrsy.tmsimg.com/assets/p22818697_e_h9_aa.jpg', - description: - 'När samtliga sex män frias för ännu en skjutning växer vreden inom polisen. Ökningen av skjutningar i Sverige ligger i topp i Europa - och nu är våldsspiralen på väg mot ett nattsvart rekord. Hur blev Sverige landet där mördare går fria?', - actors: ['Karin Mattisson', 'Ali Fegan'], - category: ['Dokumentär', 'Samhällsfrågor'], - season: 23, - episode: 5, - sub_title: 'Där mördare går fria' - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv24.se/tv24.se.config.js --output=./sites/tv24.se/tv24.se.channels.xml +// npm run grab -- --site=tv24.se + +const { parser, url } = require('./tv24.se.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-08-26', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'svt1', + xmltv_id: 'SVT1.se' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://tv24.se/x/channel/svt1/0/2022-08-26') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + axios.get.mockImplementation(url => { + if (url === 'https://tv24.se/x/b/rh7f40-1hkm/0/0') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program1.json'))) + }) + } else if (url === 'https://tv24.se/x/b/rh9dhc-1hkm/0/0') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program2.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-08-26T04:00:00.000Z', + stop: '2022-08-26T07:10:00.000Z', + title: 'Morgonstudion', + icon: 'https://jrsy.tmsimg.com/assets/p14436175_i_h9_ad.jpg', + description: + 'Dagens viktigaste nyheter och analyser med ständiga uppdateringar. Vi sänder direkt inrikes- och utrikesnyheter inklusive sport, kultur och nöje. Dessutom intervjuer med aktuella gäster. Nyhetssammanfattningar varje kvart med start kl 06.00.', + actors: ['Carolina Neurath', 'Karin Magnusson', 'Pelle Nilsson', 'Ted Wigren'] + }) + + expect(results[33]).toMatchObject({ + start: '2022-08-27T05:20:00.000Z', + stop: '2022-08-27T05:50:00.000Z', + title: 'Uppdrag granskning', + icon: 'https://jrsy.tmsimg.com/assets/p22818697_e_h9_aa.jpg', + description: + 'När samtliga sex män frias för ännu en skjutning växer vreden inom polisen. Ökningen av skjutningar i Sverige ligger i topp i Europa - och nu är våldsspiralen på väg mot ett nattsvart rekord. Hur blev Sverige landet där mördare går fria?', + actors: ['Karin Mattisson', 'Ali Fegan'], + category: ['Dokumentär', 'Samhällsfrågor'], + season: 23, + episode: 5, + sub_title: 'Där mördare går fria' + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tv2go.t-2.net/jquery.md5.js b/sites/tv2go.t-2.net/jquery.md5.js index 43518dc6..a8f351dd 100644 --- a/sites/tv2go.t-2.net/jquery.md5.js +++ b/sites/tv2go.t-2.net/jquery.md5.js @@ -1,264 +1,264 @@ -/* - * jQuery MD5 Plugin 1.2.1 - * https://github.com/blueimp/jQuery-MD5 - * - * Copyright 2010, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://creativecommons.org/licenses/MIT/ - * - * Based on - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - -/* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ -function safe_add(x, y) { - var lsw = (x & 0xffff) + (y & 0xffff), - msw = (x >> 16) + (y >> 16) + (lsw >> 16) - return (msw << 16) | (lsw & 0xffff) -} - -/* - * Bitwise rotate a 32-bit number to the left. - */ -function bit_rol(num, cnt) { - return (num << cnt) | (num >>> (32 - cnt)) -} - -/* - * These functions implement the four basic operations the algorithm uses. - */ -function md5_cmn(q, a, b, x, s, t) { - return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) -} -function md5_ff(a, b, c, d, x, s, t) { - return md5_cmn((b & c) | (~b & d), a, b, x, s, t) -} -function md5_gg(a, b, c, d, x, s, t) { - return md5_cmn((b & d) | (c & ~d), a, b, x, s, t) -} -function md5_hh(a, b, c, d, x, s, t) { - return md5_cmn(b ^ c ^ d, a, b, x, s, t) -} -function md5_ii(a, b, c, d, x, s, t) { - return md5_cmn(c ^ (b | ~d), a, b, x, s, t) -} - -/* - * Calculate the MD5 of an array of little-endian words, and a bit length. - */ -function binl_md5(x, len) { - /* append padding */ - x[len >> 5] |= 0x80 << len % 32 - x[(((len + 64) >>> 9) << 4) + 14] = len - - var i, - olda, - oldb, - oldc, - oldd, - a = 1732584193, - b = -271733879, - c = -1732584194, - d = 271733878 - - for (i = 0; i < x.length; i += 16) { - olda = a - oldb = b - oldc = c - oldd = d - - a = md5_ff(a, b, c, d, x[i], 7, -680876936) - d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) - c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) - b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) - a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) - d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) - c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) - b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) - a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) - d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) - c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) - b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) - a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) - d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) - c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) - b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) - - a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) - d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) - c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) - b = md5_gg(b, c, d, a, x[i], 20, -373897302) - a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) - d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) - c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) - b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) - a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) - d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) - c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) - b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) - a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) - d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) - c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) - b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) - - a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) - d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) - c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) - b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) - a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) - d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) - c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) - b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) - a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) - d = md5_hh(d, a, b, c, x[i], 11, -358537222) - c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) - b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) - a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) - d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) - c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) - b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) - - a = md5_ii(a, b, c, d, x[i], 6, -198630844) - d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) - c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) - b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) - a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) - d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) - c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) - b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) - a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) - d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) - c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) - b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) - a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) - d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) - c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) - b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) - - a = safe_add(a, olda) - b = safe_add(b, oldb) - c = safe_add(c, oldc) - d = safe_add(d, oldd) - } - return [a, b, c, d] -} - -/* - * Convert an array of little-endian words to a string - */ -function binl2rstr(input) { - var i, - output = '' - for (i = 0; i < input.length * 32; i += 8) { - output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff) - } - return output -} - -/* - * Convert a raw string to an array of little-endian words - * Characters >255 have their high-byte silently ignored. - */ -function rstr2binl(input) { - var i, - output = [] - output[(input.length >> 2) - 1] = undefined - for (i = 0; i < output.length; i += 1) { - output[i] = 0 - } - for (i = 0; i < input.length * 8; i += 8) { - output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32 - } - return output -} - -/* - * Calculate the MD5 of a raw string - */ -function rstr_md5(s) { - return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) -} - -/* - * Calculate the HMAC-MD5, of a key and some data (raw strings) - */ -function rstr_hmac_md5(key, data) { - var i, - bkey = rstr2binl(key), - ipad = [], - opad = [], - hash - ipad[15] = opad[15] = undefined - if (bkey.length > 16) { - bkey = binl_md5(bkey, key.length * 8) - } - for (i = 0; i < 16; i += 1) { - ipad[i] = bkey[i] ^ 0x36363636 - opad[i] = bkey[i] ^ 0x5c5c5c5c - } - hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) - return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) -} - -/* - * Convert a raw string to a hex string - */ -function rstr2hex(input) { - var hex_tab = '0123456789abcdef', - output = '', - x, - i - for (i = 0; i < input.length; i += 1) { - x = input.charCodeAt(i) - output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f) - } - return output -} - -/* - * Encode a string as utf-8 - */ -function str2rstr_utf8(input) { - return unescape(encodeURIComponent(input)) -} - -/* - * Take string arguments and return either raw or hex encoded strings - */ -function raw_md5(s) { - return rstr_md5(str2rstr_utf8(s)) -} -function hex_md5(s) { - return rstr2hex(raw_md5(s)) -} -function raw_hmac_md5(k, d) { - return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)) -} -function hex_hmac_md5(k, d) { - return rstr2hex(raw_hmac_md5(k, d)) -} - -module.exports = function (string, key, raw) { - if (!key) { - if (!raw) { - return hex_md5(string) - } else { - return raw_md5(string) - } - } - if (!raw) { - return hex_hmac_md5(key, string) - } else { - return raw_hmac_md5(key, string) - } -} +/* + * jQuery MD5 Plugin 1.2.1 + * https://github.com/blueimp/jQuery-MD5 + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://creativecommons.org/licenses/MIT/ + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) { + var lsw = (x & 0xffff) + (y & 0xffff), + msw = (x >> 16) + (y >> 16) + (lsw >> 16) + return (msw << 16) | (lsw & 0xffff) +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) +} +function md5_ff(a, b, c, d, x, s, t) { + return md5_cmn((b & c) | (~b & d), a, b, x, s, t) +} +function md5_gg(a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & ~d), a, b, x, s, t) +} +function md5_hh(a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t) +} +function md5_ii(a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | ~d), a, b, x, s, t) +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ +function binl_md5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << len % 32 + x[(((len + 64) >>> 9) << 4) + 14] = len + + var i, + olda, + oldb, + oldc, + oldd, + a = 1732584193, + b = -271733879, + c = -1732584194, + d = 271733878 + + for (i = 0; i < x.length; i += 16) { + olda = a + oldb = b + oldc = c + oldd = d + + a = md5_ff(a, b, c, d, x[i], 7, -680876936) + d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) + c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) + b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) + a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) + d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) + c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) + b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) + a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) + d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) + c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) + b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) + a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) + d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) + c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) + b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) + + a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) + d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) + c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) + b = md5_gg(b, c, d, a, x[i], 20, -373897302) + a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) + d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) + c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) + b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) + a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) + d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) + c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) + b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) + a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) + d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) + c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) + b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) + + a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) + d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) + c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) + b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) + a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) + d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) + c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) + b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) + a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) + d = md5_hh(d, a, b, c, x[i], 11, -358537222) + c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) + b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) + a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) + d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) + c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) + b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) + + a = md5_ii(a, b, c, d, x[i], 6, -198630844) + d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) + c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) + b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) + a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) + d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) + c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) + b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) + a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) + d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) + c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) + b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) + a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) + d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) + c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) + b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) + + a = safe_add(a, olda) + b = safe_add(b, oldb) + c = safe_add(c, oldc) + d = safe_add(d, oldd) + } + return [a, b, c, d] +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2rstr(input) { + var i, + output = '' + for (i = 0; i < input.length * 32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff) + } + return output +} + +/* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binl(input) { + var i, + output = [] + output[(input.length >> 2) - 1] = undefined + for (i = 0; i < output.length; i += 1) { + output[i] = 0 + } + for (i = 0; i < input.length * 8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32 + } + return output +} + +/* + * Calculate the MD5 of a raw string + */ +function rstr_md5(s) { + return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) +} + +/* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ +function rstr_hmac_md5(key, data) { + var i, + bkey = rstr2binl(key), + ipad = [], + opad = [], + hash + ipad[15] = opad[15] = undefined + if (bkey.length > 16) { + bkey = binl_md5(bkey, key.length * 8) + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636 + opad[i] = bkey[i] ^ 0x5c5c5c5c + } + hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) + return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) { + var hex_tab = '0123456789abcdef', + output = '', + x, + i + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i) + output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f) + } + return output +} + +/* + * Encode a string as utf-8 + */ +function str2rstr_utf8(input) { + return unescape(encodeURIComponent(input)) +} + +/* + * Take string arguments and return either raw or hex encoded strings + */ +function raw_md5(s) { + return rstr_md5(str2rstr_utf8(s)) +} +function hex_md5(s) { + return rstr2hex(raw_md5(s)) +} +function raw_hmac_md5(k, d) { + return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)) +} +function hex_hmac_md5(k, d) { + return rstr2hex(raw_hmac_md5(k, d)) +} + +module.exports = function (string, key, raw) { + if (!key) { + if (!raw) { + return hex_md5(string) + } else { + return raw_md5(string) + } + } + if (!raw) { + return hex_hmac_md5(key, string) + } else { + return raw_hmac_md5(key, string) + } +} diff --git a/sites/tv2go.t-2.net/tv2go.t-2.net.config.js b/sites/tv2go.t-2.net/tv2go.t-2.net.config.js index 53300e5f..2391d951 100644 --- a/sites/tv2go.t-2.net/tv2go.t-2.net.config.js +++ b/sites/tv2go.t-2.net/tv2go.t-2.net.config.js @@ -1,126 +1,126 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const md5 = require('./jquery.md5') - -const API = { - locale: 'sl-SI', - version: '9.4', - format: 'json', - uuid: '464830403846070', - token: '6dace810-55d5-11e3-949a-0800200c9a66' -} - -const config = { - site: 'tv2go.t-2.net', - days: 2, - url({ date, channel }) { - const data = config.request.data({ date, channel }) - const endpoint = 'client/tv/getEpg' - const hash = generateHash(data, endpoint) - - return `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}` - }, - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - data({ date, channel }) { - const channelId = parseInt(channel.site_id) - - return { - locale: API.locale, - channelId: [channelId], - startTime: date.valueOf(), - endTime: date.add(1, 'd').valueOf(), - imageInfo: [{ height: 500, width: 1100 }], - includeBookmarks: false, - includeShow: true - } - } - }, - parser({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - category: parseCategory(item), - description: parseDescription(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const data = { - locale: API.locale, - type: 'TV', - imageInfo: [{ type: 'DARK', height: 70, width: 98 }] - } - const endpoint = 'client/channels/list' - const hash = generateHash(data, endpoint) - const response = await axios - .post( - `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}`, - data, - { - headers: { - 'Content-Type': 'application/json' - } - } - ) - .catch(console.log) - - return response.data.channels.map(item => { - return { - lang: 'sl', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseStart(item) { - return dayjs(parseInt(item.startTimestamp)) -} - -function parseStop(item) { - return dayjs(parseInt(item.endTimestamp)) -} - -function parseIcon(item) { - return item.images && item.images[0] ? `https://tv2go.t-2.net${item.images[0].url}` : null -} - -function parseCategory(item) { - return item.show && Array.isArray(item.show.genres) ? item.show.genres.map(c => c.name) : [] -} - -function parseDescription(item) { - return item.show ? item.show.shortDescription : null -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch (e) { - return [] - } - if (!data || !Array.isArray(data.entries)) return [] - - return data.entries -} - -function generateHash(data, endpoint) { - const salt = `${API.token}${API.version}${API.format}${API.uuid}` - - return md5(salt + endpoint + JSON.stringify(data)) -} - -module.exports = config +const axios = require('axios') +const dayjs = require('dayjs') +const md5 = require('./jquery.md5') + +const API = { + locale: 'sl-SI', + version: '9.4', + format: 'json', + uuid: '464830403846070', + token: '6dace810-55d5-11e3-949a-0800200c9a66' +} + +const config = { + site: 'tv2go.t-2.net', + days: 2, + url({ date, channel }) { + const data = config.request.data({ date, channel }) + const endpoint = 'client/tv/getEpg' + const hash = generateHash(data, endpoint) + + return `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}` + }, + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + data({ date, channel }) { + const channelId = parseInt(channel.site_id) + + return { + locale: API.locale, + channelId: [channelId], + startTime: date.valueOf(), + endTime: date.add(1, 'd').valueOf(), + imageInfo: [{ height: 500, width: 1100 }], + includeBookmarks: false, + includeShow: true + } + } + }, + parser({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + category: parseCategory(item), + description: parseDescription(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const data = { + locale: API.locale, + type: 'TV', + imageInfo: [{ type: 'DARK', height: 70, width: 98 }] + } + const endpoint = 'client/channels/list' + const hash = generateHash(data, endpoint) + const response = await axios + .post( + `https://tv2go.t-2.net/Catherine/api/${API.version}/${API.format}/${API.uuid}/${hash}/${endpoint}`, + data, + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + .catch(console.log) + + return response.data.channels.map(item => { + return { + lang: 'sl', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseStart(item) { + return dayjs(parseInt(item.startTimestamp)) +} + +function parseStop(item) { + return dayjs(parseInt(item.endTimestamp)) +} + +function parseIcon(item) { + return item.images && item.images[0] ? `https://tv2go.t-2.net${item.images[0].url}` : null +} + +function parseCategory(item) { + return item.show && Array.isArray(item.show.genres) ? item.show.genres.map(c => c.name) : [] +} + +function parseDescription(item) { + return item.show ? item.show.shortDescription : null +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch (e) { + return [] + } + if (!data || !Array.isArray(data.entries)) return [] + + return data.entries +} + +function generateHash(data, endpoint) { + const salt = `${API.token}${API.version}${API.format}${API.uuid}` + + return md5(salt + endpoint + JSON.stringify(data)) +} + +module.exports = config diff --git a/sites/tv2go.t-2.net/tv2go.t-2.net.test.js b/sites/tv2go.t-2.net/tv2go.t-2.net.test.js index 2d69860d..5adde245 100644 --- a/sites/tv2go.t-2.net/tv2go.t-2.net.test.js +++ b/sites/tv2go.t-2.net/tv2go.t-2.net.test.js @@ -1,70 +1,70 @@ -// npm run channels:parse -- --config=./sites/tv2go.t-2.net/tv2go.t-2.net.config.js --output=./sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml -// npm run grab -- --site=tv2go.t-2.net - -const { parser, url, request } = require('./tv2go.t-2.net.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1000259', - xmltv_id: 'TVSlovenija1.si' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json' - }) -}) - -it('can generate valid request data', () => { - expect(request.data({ date, channel })).toMatchObject({ - locale: 'sl-SI', - channelId: [1000259], - startTime: 1637280000000, - endTime: 1637366400000, - imageInfo: [{ height: 500, width: 1100 }], - includeBookmarks: false, - includeShow: true - }) -}) - -it('can parse response', () => { - const content = - '{"entries":[{"channelId":1000259,"startTimestamp":"1637283000000","endTimestamp":"1637284500000","name":"Dnevnik Slovencev v Italiji","nameSingleLine":"Dnevnik Slovencev v Italiji","description":"Informativni","images":[{"url":"/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg","width":1008,"height":720,"averageColor":[143,147,161]}],"show":{"id":51991133,"title":"Dnevnik Slovencev v Italiji","originalTitle":"Dnevnik Slovencev v Italiji","shortDescription":"Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.","longDescription":"Pomembno ogledalo vsakdana, v katerem opozarjajo na težave, s katerimi se soočajo, predstavljajo pa tudi pestro kulturno, športno in družbeno življenje slovenske narodne skupnosti. V oddajo so vključene tudi novice iz matične domovine.","type":{"id":10,"name":"Show"},"productionFrom":"1609502400000","countries":[{"id":"SI","name":"Slovenija"}],"languages":[{"languageId":2,"name":"Slovenščina"}],"genres":[{"id":1000002,"name":"Informativni"}]}}]}' - const result = parser({ content, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-19T00:50:00.000Z', - stop: '2021-11-19T01:15:00.000Z', - title: 'Dnevnik Slovencev v Italiji', - category: ['Informativni'], - description: - 'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.', - icon: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: 'Invalid API client identifier' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tv2go.t-2.net/tv2go.t-2.net.config.js --output=./sites/tv2go.t-2.net/tv2go.t-2.net.channels.xml +// npm run grab -- --site=tv2go.t-2.net + +const { parser, url, request } = require('./tv2go.t-2.net.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1000259', + xmltv_id: 'TVSlovenija1.si' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://tv2go.t-2.net/Catherine/api/9.4/json/464830403846070/d79cf4dc84f2131689f426956b8d40de/client/tv/getEpg' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/json' + }) +}) + +it('can generate valid request data', () => { + expect(request.data({ date, channel })).toMatchObject({ + locale: 'sl-SI', + channelId: [1000259], + startTime: 1637280000000, + endTime: 1637366400000, + imageInfo: [{ height: 500, width: 1100 }], + includeBookmarks: false, + includeShow: true + }) +}) + +it('can parse response', () => { + const content = + '{"entries":[{"channelId":1000259,"startTimestamp":"1637283000000","endTimestamp":"1637284500000","name":"Dnevnik Slovencev v Italiji","nameSingleLine":"Dnevnik Slovencev v Italiji","description":"Informativni","images":[{"url":"/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg","width":1008,"height":720,"averageColor":[143,147,161]}],"show":{"id":51991133,"title":"Dnevnik Slovencev v Italiji","originalTitle":"Dnevnik Slovencev v Italiji","shortDescription":"Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.","longDescription":"Pomembno ogledalo vsakdana, v katerem opozarjajo na težave, s katerimi se soočajo, predstavljajo pa tudi pestro kulturno, športno in družbeno življenje slovenske narodne skupnosti. V oddajo so vključene tudi novice iz matične domovine.","type":{"id":10,"name":"Show"},"productionFrom":"1609502400000","countries":[{"id":"SI","name":"Slovenija"}],"languages":[{"languageId":2,"name":"Slovenščina"}],"genres":[{"id":1000002,"name":"Informativni"}]}}]}' + const result = parser({ content, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-19T00:50:00.000Z', + stop: '2021-11-19T01:15:00.000Z', + title: 'Dnevnik Slovencev v Italiji', + category: ['Informativni'], + description: + 'Dnevnik Slovencev v Italiji je informativna oddaja, v kateri novinarji poročajo predvsem o dnevnih dogodkih med Slovenci v Italiji.', + icon: 'https://tv2go.t-2.net/static/media/img/epg/max_crop/EPG_IMG_2927405.jpg' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: 'Invalid API client identifier' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tva.tv/tva.tv.config.js b/sites/tva.tv/tva.tv.config.js index f2a6a670..41b9a756 100644 --- a/sites/tva.tv/tva.tv.config.js +++ b/sites/tva.tv/tva.tv.config.js @@ -1,58 +1,58 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tva.tv', - days: 2, - url: function ({ date, channel }) { - return `https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=${ - channel.site_id - }&pivot_date=${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.name, - description: item.description, - start: dayjs(item.start_at), - stop: dayjs(item.end_at) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get( - 'https://api.ott.tva.tv/v1/channels?client_id=66797942-ff54-46cb-a109-3bae7c855370&client_version=0.0.1&expand%5Bchannel%5D=images&locale=en-GB&page%5Blimit%5D=100&page%5Boffset%5D=0&timezone=10800', - { - headers: { - Origin: 'https://tva.tv' - } - } - ) - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let item of data.data) { - channels.push({ - lang: 'fa', - site_id: item.id, - name: item.name, - xmltv_id: item.slug - }) - } - - return channels - } -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data)) return [] - - return data.data -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tva.tv', + days: 2, + url: function ({ date, channel }) { + return `https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=${ + channel.site_id + }&pivot_date=${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.name, + description: item.description, + start: dayjs(item.start_at), + stop: dayjs(item.end_at) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get( + 'https://api.ott.tva.tv/v1/channels?client_id=66797942-ff54-46cb-a109-3bae7c855370&client_version=0.0.1&expand%5Bchannel%5D=images&locale=en-GB&page%5Blimit%5D=100&page%5Boffset%5D=0&timezone=10800', + { + headers: { + Origin: 'https://tva.tv' + } + } + ) + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let item of data.data) { + channels.push({ + lang: 'fa', + site_id: item.id, + name: item.name, + xmltv_id: item.slug + }) + } + + return channels + } +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data)) return [] + + return data.data +} diff --git a/sites/tva.tv/tva.tv.test.js b/sites/tva.tv/tva.tv.test.js index bd9504f1..6d6d1fa2 100644 --- a/sites/tva.tv/tva.tv.test.js +++ b/sites/tva.tv/tva.tv.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/tva.tv/tva.tv.config.js --output=./sites/tva.tv/tva.tv.channels.xml -// npm run grab -- --site=tva.tv - -const { parser, url } = require('./tva.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '6fcc0a2e-1135-482c-b054-08a96e68b758', - xmltv_id: 'IRIB2.ir' -} -const content = - '{"data":[{"id":"c0667229-eaf8-472f-8ba7-ad4417348baf","start_at":"2021-11-24T00:20:39.000Z","end_at":"2021-11-24T00:32:11.000Z","description":"تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289","name":"میان برنامه","subtitle":"","season_number":null,"episode_number":null,"channel_id":"6fcc0a2e-1135-482c-b054-08a96e68b758","program_id":"e495c06e-80de-46ee-9120-619631f554d9","competition_id":null,"object":"program_event","cast_members":[],"genres":[],"images":[],"program_type":null,"certification_ratings":[]}]}' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=6fcc0a2e-1135-482c-b054-08a96e68b758&pivot_date=2021-11-25' - ) -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T00:20:39.000Z', - stop: '2021-11-24T00:32:11.000Z', - title: 'میان برنامه', - description: 'تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"errors":[{"id":"ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c","code":"not_found","links":{"about":{"href":"https://app.bugsnag.com/jeytv/API/errors?filters[event.since][]=30d&filters[user.name][]=ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c"}},"title":"Requested resource was not found","fallback_message":null,"object":"error"}],"meta":{"status":404}}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tva.tv/tva.tv.config.js --output=./sites/tva.tv/tva.tv.channels.xml +// npm run grab -- --site=tva.tv + +const { parser, url } = require('./tva.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '6fcc0a2e-1135-482c-b054-08a96e68b758', + xmltv_id: 'IRIB2.ir' +} +const content = + '{"data":[{"id":"c0667229-eaf8-472f-8ba7-ad4417348baf","start_at":"2021-11-24T00:20:39.000Z","end_at":"2021-11-24T00:32:11.000Z","description":"تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289","name":"میان برنامه","subtitle":"","season_number":null,"episode_number":null,"channel_id":"6fcc0a2e-1135-482c-b054-08a96e68b758","program_id":"e495c06e-80de-46ee-9120-619631f554d9","competition_id":null,"object":"program_event","cast_members":[],"genres":[],"images":[],"program_type":null,"certification_ratings":[]}]}' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.ott.tva.tv/v2/epg/program_events.json?channel_id=6fcc0a2e-1135-482c-b054-08a96e68b758&pivot_date=2021-11-25' + ) +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T00:20:39.000Z', + stop: '2021-11-24T00:32:11.000Z', + title: 'میان برنامه', + description: 'تلفن های شبکه 5 سیما: تلفن: 23511000 -تلفن گویا:2786500 نمابر:23511289' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"errors":[{"id":"ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c","code":"not_found","links":{"about":{"href":"https://app.bugsnag.com/jeytv/API/errors?filters[event.since][]=30d&filters[user.name][]=ebbebfb7-ffb0-4e0b-bcfb-1d9cd3e6c03c"}},"title":"Requested resource was not found","fallback_message":null,"object":"error"}],"meta":{"status":404}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvarenasport.com/tvarenasport.com.config.js b/sites/tvarenasport.com/tvarenasport.com.config.js index 53d91cf1..d7b0e313 100644 --- a/sites/tvarenasport.com/tvarenasport.com.config.js +++ b/sites/tvarenasport.com/tvarenasport.com.config.js @@ -1,50 +1,50 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvarenasport.com', - days: 2, - url: function ({ date }) { - return `https://www.tvarenasport.com/api/schedule?date=${date.format('DD-MM-YYYY')}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title.trim(), - category: item.league, - description: item.sport.trim(), - start: dayjs(item.start), - stop: dayjs(item.end) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.tvarenasport.com/api/schedule') - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let id in data.channels) { - const item = data.channels[id] - channels.push({ - lang: 'sr', - site_id: id, - name: item.name - }) - } - - return channels - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.items)) return [] - - return data.items.filter(i => i.group === channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvarenasport.com', + days: 2, + url: function ({ date }) { + return `https://www.tvarenasport.com/api/schedule?date=${date.format('DD-MM-YYYY')}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title.trim(), + category: item.league, + description: item.sport.trim(), + start: dayjs(item.start), + stop: dayjs(item.end) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.tvarenasport.com/api/schedule') + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let id in data.channels) { + const item = data.channels[id] + channels.push({ + lang: 'sr', + site_id: id, + name: item.name + }) + } + + return channels + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.items)) return [] + + return data.items.filter(i => i.group === channel.site_id) +} diff --git a/sites/tvarenasport.com/tvarenasport.com.test.js b/sites/tvarenasport.com/tvarenasport.com.test.js index 9460a3a6..4781a9ea 100644 --- a/sites/tvarenasport.com/tvarenasport.com.test.js +++ b/sites/tvarenasport.com/tvarenasport.com.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/tvarenasport.com/tvarenasport.com.config.js --output=./sites/tvarenasport.com/tvarenasport.com.channels.xml --set=country:rs -// npm run grab -- --site=tvarenasport.com - -const { parser, url } = require('./tvarenasport.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '380', - xmltv_id: 'ArenaSport1.rs' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvarenasport.com/api/schedule?date=17-11-2021') -}) - -it('can parse response', () => { - const content = - '{"items":[{"id":2857,"title":"Crvena zvezda mts - Partizan NIS","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:30:00Z","sport":"ABA LIGA","league":"Ko\u0161arka","group":"380","isLive":false,"doNotMiss":false,"domain":"srb"},{"id":3155,"title":"Sao Paulo - Flamengo","start":"2021-11-17T00:00:00Z","end":"2021-11-17T02:00:00Z","sport":"BRAZILSKA LIGA","league":"Fudbal","group":"381","isLive":false,"doNotMiss":false,"domain":"srb"}]}' - const result = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T23:30:00.000Z', - stop: '2021-11-17T01:30:00.000Z', - title: 'Crvena zvezda mts - Partizan NIS', - category: 'Ko\u0161arka', - description: 'ABA LIGA' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvarenasport.com/tvarenasport.com.config.js --output=./sites/tvarenasport.com/tvarenasport.com.channels.xml --set=country:rs +// npm run grab -- --site=tvarenasport.com + +const { parser, url } = require('./tvarenasport.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '380', + xmltv_id: 'ArenaSport1.rs' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvarenasport.com/api/schedule?date=17-11-2021') +}) + +it('can parse response', () => { + const content = + '{"items":[{"id":2857,"title":"Crvena zvezda mts - Partizan NIS","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:30:00Z","sport":"ABA LIGA","league":"Ko\u0161arka","group":"380","isLive":false,"doNotMiss":false,"domain":"srb"},{"id":3155,"title":"Sao Paulo - Flamengo","start":"2021-11-17T00:00:00Z","end":"2021-11-17T02:00:00Z","sport":"BRAZILSKA LIGA","league":"Fudbal","group":"381","isLive":false,"doNotMiss":false,"domain":"srb"}]}' + const result = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T23:30:00.000Z', + stop: '2021-11-17T01:30:00.000Z', + title: 'Crvena zvezda mts - Partizan NIS', + category: 'Ko\u0161arka', + description: 'ABA LIGA' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"channels":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvarenasport.hr/tvarenasport.hr.config.js b/sites/tvarenasport.hr/tvarenasport.hr.config.js index 518d97c4..a6ad4706 100644 --- a/sites/tvarenasport.hr/tvarenasport.hr.config.js +++ b/sites/tvarenasport.hr/tvarenasport.hr.config.js @@ -1,50 +1,50 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvarenasport.hr', - days: 2, - url: function ({ date }) { - return `https://www.tvarenasport.hr/api/schedule?date=${date.format('DD-MM-YYYY')}` - }, - parser: function ({ content, channel }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: item.title.trim(), - category: item.sport, - description: item.league.trim(), - start: dayjs(item.start), - stop: dayjs(item.end) - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.tvarenasport.hr/api/schedule') - .then(r => r.data) - .catch(console.log) - - const channels = [] - for (let id in data.channels) { - const item = data.channels[id] - channels.push({ - lang: 'hr', - site_id: id, - name: item.name - }) - } - - return channels - } -} - -function parseItems(content, channel) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.items)) return [] - - return data.items.filter(i => i.group === channel.site_id) -} +const axios = require('axios') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvarenasport.hr', + days: 2, + url: function ({ date }) { + return `https://www.tvarenasport.hr/api/schedule?date=${date.format('DD-MM-YYYY')}` + }, + parser: function ({ content, channel }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: item.title.trim(), + category: item.sport, + description: item.league.trim(), + start: dayjs(item.start), + stop: dayjs(item.end) + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.tvarenasport.hr/api/schedule') + .then(r => r.data) + .catch(console.log) + + const channels = [] + for (let id in data.channels) { + const item = data.channels[id] + channels.push({ + lang: 'hr', + site_id: id, + name: item.name + }) + } + + return channels + } +} + +function parseItems(content, channel) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.items)) return [] + + return data.items.filter(i => i.group === channel.site_id) +} diff --git a/sites/tvarenasport.hr/tvarenasport.hr.test.js b/sites/tvarenasport.hr/tvarenasport.hr.test.js index 30cf22e2..966e6220 100644 --- a/sites/tvarenasport.hr/tvarenasport.hr.test.js +++ b/sites/tvarenasport.hr/tvarenasport.hr.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/tvarenasport.hr/tvarenasport.hr.config.js --output=./sites/tvarenasport.hr/tvarenasport.hr.channels.xml -// npm run grab -- --site=tvarenasport.hr - -const { parser, url } = require('./tvarenasport.hr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '380', - xmltv_id: 'ArenaSport1Croatia.hr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvarenasport.hr/api/schedule?date=17-11-2021') -}) - -it('can parse response', () => { - const content = - '{"items":[{"id":6104,"title":"NAJAVA PROGRAMA","start":"2021-11-16T23:00:00Z","end":"2021-11-17T23:00:00Z","sport":"Najava programa","league":"NAJAVA PROGRAMA","group":"1294","isLive":false,"doNotMiss":false,"domain":"cro"},{"id":6000,"title":" DIJON - UNICAJA","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:00:00Z","sport":"Košarka","league":" LIGA PRVAKA","group":"380","isLive":false,"doNotMiss":false,"domain":"cro"}]}' - const result = parser({ channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-16T23:30:00.000Z', - stop: '2021-11-17T01:00:00.000Z', - title: 'DIJON - UNICAJA', - category: 'Košarka', - description: 'LIGA PRVAKA' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"channels":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvarenasport.hr/tvarenasport.hr.config.js --output=./sites/tvarenasport.hr/tvarenasport.hr.channels.xml +// npm run grab -- --site=tvarenasport.hr + +const { parser, url } = require('./tvarenasport.hr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '380', + xmltv_id: 'ArenaSport1Croatia.hr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvarenasport.hr/api/schedule?date=17-11-2021') +}) + +it('can parse response', () => { + const content = + '{"items":[{"id":6104,"title":"NAJAVA PROGRAMA","start":"2021-11-16T23:00:00Z","end":"2021-11-17T23:00:00Z","sport":"Najava programa","league":"NAJAVA PROGRAMA","group":"1294","isLive":false,"doNotMiss":false,"domain":"cro"},{"id":6000,"title":" DIJON - UNICAJA","start":"2021-11-16T23:30:00Z","end":"2021-11-17T01:00:00Z","sport":"Košarka","league":" LIGA PRVAKA","group":"380","isLive":false,"doNotMiss":false,"domain":"cro"}]}' + const result = parser({ channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-16T23:30:00.000Z', + stop: '2021-11-17T01:00:00.000Z', + title: 'DIJON - UNICAJA', + category: 'Košarka', + description: 'LIGA PRVAKA' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"channels":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js index 8aecfdfd..515da300 100644 --- a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js +++ b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(timezone) - -module.exports = { - site: 'tvcubana.icrt.cu', - days: 2, - url({ channel, date }) { - const daysOfWeek = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado'] - - return `https://www.tvcubana.icrt.cu/cartv/${channel.site_id}/${daysOfWeek[date.day()]}.php` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.description, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.tz(item.eventInitialDateTime, 'America/Havana') -} - -function parseStop(item) { - return dayjs.tz(item.eventEndDateTime, 'America/Havana') -} - -function parseItems(content) { - let data - try { - data = JSON.parse(content) - } catch (error) { - return [] - } - if (!data || !Array.isArray(data)) return [] - - return data -} +const dayjs = require('dayjs') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(timezone) + +module.exports = { + site: 'tvcubana.icrt.cu', + days: 2, + url({ channel, date }) { + const daysOfWeek = ['domingo', 'lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado'] + + return `https://www.tvcubana.icrt.cu/cartv/${channel.site_id}/${daysOfWeek[date.day()]}.php` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.description, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.tz(item.eventInitialDateTime, 'America/Havana') +} + +function parseStop(item) { + return dayjs.tz(item.eventEndDateTime, 'America/Havana') +} + +function parseItems(content) { + let data + try { + data = JSON.parse(content) + } catch (error) { + return [] + } + if (!data || !Array.isArray(data)) return [] + + return data +} diff --git a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js index d04759db..4789b0d5 100644 --- a/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js +++ b/sites/tvcubana.icrt.cu/tvcubana.icrt.cu.test.js @@ -1,52 +1,52 @@ -// npm run grab -- --site=tvcubana.icrt.cu - -const { parser, url } = require('./tvcubana.icrt.cu.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'cv', - xmltv_id: 'CubavisionNacional.cu' -} -const content = - '[{"eventId":"6169c2300ad38b0a8d9e3760","title":"CARIBE NOTICIAS","description":"EMISI\\u00d3N DE CIERRE.","eventInitialDate":"2021-11-22T00:00:00","eventEndDate":"2021-11-22T00:00:00","idFromEprog":"5c096ea5bad1b202541503cf","extendedDescription":"","transmission":"Estreno","pid":"","space":"CARIBE NOTICIAS","eventStartTime":{"value":{"ticks":24000000000,"days":0,"hours":0,"milliseconds":0,"minutes":40,"seconds":0,"totalDays":0.027777777777777776,"totalHours":0.6666666666666666,"totalMilliseconds":2400000,"totalMinutes":40,"totalSeconds":2400},"hasValue":true},"eventEndTime":{"value":{"ticks":30000000000,"days":0,"hours":0,"milliseconds":0,"minutes":50,"seconds":0,"totalDays":0.034722222222222224,"totalHours":0.8333333333333334,"totalMilliseconds":3000000,"totalMinutes":50,"totalSeconds":3000},"hasValue":true},"eventDuration":"00:10:00","channelName":"Cubavisi\\u00f3n","eventInitialDateTime":"2021-11-22T00:40:00","eventEndDateTime":"2021-11-22T00:50:00","isEventWithNegativeDuration":false,"isEventWithDurationOver24Hrs":false,"isEventWithTextOverLength":false,"created":"2021-11-22T10:32:27.476824","id":5309687}]' - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php') -}) - -it('can generate valid url for next day', () => { - expect(url({ channel, date: date.add(2, 'd') })).toBe( - 'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - expect(result).toMatchObject([ - { - start: '2021-11-22T05:40:00.000Z', - stop: '2021-11-22T05:50:00.000Z', - title: 'CARIBE NOTICIAS', - description: 'EMISIÓN DE CIERRE.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvcubana.icrt.cu + +const { parser, url } = require('./tvcubana.icrt.cu.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-22', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'cv', + xmltv_id: 'CubavisionNacional.cu' +} +const content = + '[{"eventId":"6169c2300ad38b0a8d9e3760","title":"CARIBE NOTICIAS","description":"EMISI\\u00d3N DE CIERRE.","eventInitialDate":"2021-11-22T00:00:00","eventEndDate":"2021-11-22T00:00:00","idFromEprog":"5c096ea5bad1b202541503cf","extendedDescription":"","transmission":"Estreno","pid":"","space":"CARIBE NOTICIAS","eventStartTime":{"value":{"ticks":24000000000,"days":0,"hours":0,"milliseconds":0,"minutes":40,"seconds":0,"totalDays":0.027777777777777776,"totalHours":0.6666666666666666,"totalMilliseconds":2400000,"totalMinutes":40,"totalSeconds":2400},"hasValue":true},"eventEndTime":{"value":{"ticks":30000000000,"days":0,"hours":0,"milliseconds":0,"minutes":50,"seconds":0,"totalDays":0.034722222222222224,"totalHours":0.8333333333333334,"totalMilliseconds":3000000,"totalMinutes":50,"totalSeconds":3000},"hasValue":true},"eventDuration":"00:10:00","channelName":"Cubavisi\\u00f3n","eventInitialDateTime":"2021-11-22T00:40:00","eventEndDateTime":"2021-11-22T00:50:00","isEventWithNegativeDuration":false,"isEventWithDurationOver24Hrs":false,"isEventWithTextOverLength":false,"created":"2021-11-22T10:32:27.476824","id":5309687}]' + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.tvcubana.icrt.cu/cartv/cv/lunes.php') +}) + +it('can generate valid url for next day', () => { + expect(url({ channel, date: date.add(2, 'd') })).toBe( + 'https://www.tvcubana.icrt.cu/cartv/cv/miercoles.php' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + expect(result).toMatchObject([ + { + start: '2021-11-22T05:40:00.000Z', + stop: '2021-11-22T05:50:00.000Z', + title: 'CARIBE NOTICIAS', + description: 'EMISIÓN DE CIERRE.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvgids.nl/tvgids.nl.config.js b/sites/tvgids.nl/tvgids.nl.config.js index 8c2e13f4..b0524358 100644 --- a/sites/tvgids.nl/tvgids.nl.config.js +++ b/sites/tvgids.nl/tvgids.nl.config.js @@ -1,87 +1,87 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvgids.nl', - days: 2, - url: function ({ date, channel }) { - const path = - DateTime.now().day === DateTime.fromMillis(date.valueOf()).day - ? '' - : `${date.format('DD-MM-YYYY')}/` - - return `https://www.tvgids.nl/gids/${path}${channel.site_id}` - }, - parser: function ({ content, date }) { - date = date.subtract(1, 'd') - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.tvgids.nl/gids/') - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(data) - - const channels = [] - $('#channel-container > div').each((i, el) => { - channels.push({ - site_id: $(el).find('a').attr('id'), - name: $(el).find('img').attr('title'), - lang: 'nl' - }) - }) - - return channels - } -} - -function parseTitle($item) { - return $item('.program__title').text().trim() -} - -function parseDescription($item) { - return $item('.program__text').text().trim() -} - -function parseIcon($item) { - return $item('.program__thumbnail').data('src') -} - -function parseStart($item, date) { - const time = $item('.program__starttime').clone().children().remove().end().text().trim() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'Europe/Amsterdam' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.guide__guide .program').toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tvgids.nl', + days: 2, + url: function ({ date, channel }) { + const path = + DateTime.now().day === DateTime.fromMillis(date.valueOf()).day + ? '' + : `${date.format('DD-MM-YYYY')}/` + + return `https://www.tvgids.nl/gids/${path}${channel.site_id}` + }, + parser: function ({ content, date }) { + date = date.subtract(1, 'd') + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.tvgids.nl/gids/') + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(data) + + const channels = [] + $('#channel-container > div').each((i, el) => { + channels.push({ + site_id: $(el).find('a').attr('id'), + name: $(el).find('img').attr('title'), + lang: 'nl' + }) + }) + + return channels + } +} + +function parseTitle($item) { + return $item('.program__title').text().trim() +} + +function parseDescription($item) { + return $item('.program__text').text().trim() +} + +function parseIcon($item) { + return $item('.program__thumbnail').data('src') +} + +function parseStart($item, date) { + const time = $item('.program__starttime').clone().children().remove().end().text().trim() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'Europe/Amsterdam' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.guide__guide .program').toArray() +} diff --git a/sites/tvgids.nl/tvgids.nl.test.js b/sites/tvgids.nl/tvgids.nl.test.js index d30af52b..839976ac 100644 --- a/sites/tvgids.nl/tvgids.nl.test.js +++ b/sites/tvgids.nl/tvgids.nl.test.js @@ -1,61 +1,61 @@ -// npm run channels:parse -- --config=./sites/tvgids.nl/tvgids.nl.config.js --output=./sites/tvgids.nl/tvgids.nl.channels.xml -// npm run grab -- --site=tvgids.nl - -const { parser, url } = require('./tvgids.nl.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'npo1', - xmltv_id: 'NPO1.nl' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://www.tvgids.nl/gids/13-06-2023/npo1') -}) - -it('can generate valid url for today', () => { - const today = dayjs().startOf('d') - - expect(url({ date: today, channel })).toBe('https://www.tvgids.nl/gids/npo1') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-06-12T21:57:00.000Z', - stop: '2023-06-12T22:58:00.000Z', - title: 'Op1', - icon: 'https://tvgidsassets.nl/v301/upload/o/carrousel/op1-451542641.jpg', - description: "Talkshow met wisselende presentatieduo's, live vanuit Amsterdam." - }) - - expect(results[61]).toMatchObject({ - start: '2023-06-14T00:18:00.000Z', - stop: '2023-06-14T00:48:00.000Z', - title: 'NOS Journaal', - icon: 'https://tvgidsassets.nl/v301/upload/n/carrousel/nos-journaal-452818771.jpg', - description: - 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor vandaag.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - date - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvgids.nl/tvgids.nl.config.js --output=./sites/tvgids.nl/tvgids.nl.channels.xml +// npm run grab -- --site=tvgids.nl + +const { parser, url } = require('./tvgids.nl.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-06-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'npo1', + xmltv_id: 'NPO1.nl' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://www.tvgids.nl/gids/13-06-2023/npo1') +}) + +it('can generate valid url for today', () => { + const today = dayjs().startOf('d') + + expect(url({ date: today, channel })).toBe('https://www.tvgids.nl/gids/npo1') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-06-12T21:57:00.000Z', + stop: '2023-06-12T22:58:00.000Z', + title: 'Op1', + icon: 'https://tvgidsassets.nl/v301/upload/o/carrousel/op1-451542641.jpg', + description: "Talkshow met wisselende presentatieduo's, live vanuit Amsterdam." + }) + + expect(results[61]).toMatchObject({ + start: '2023-06-14T00:18:00.000Z', + stop: '2023-06-14T00:48:00.000Z', + title: 'NOS Journaal', + icon: 'https://tvgidsassets.nl/v301/upload/n/carrousel/nos-journaal-452818771.jpg', + description: + 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor vandaag.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + date + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvguide.com/tvguide.com.config.js b/sites/tvguide.com/tvguide.com.config.js index c9a1f8e7..e08b3095 100644 --- a/sites/tvguide.com/tvguide.com.config.js +++ b/sites/tvguide.com/tvguide.com.config.js @@ -1,71 +1,71 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'tvguide.com', - days: 2, - url: function ({ date, channel }) { - const [providerId, channelSourceIds] = channel.site_id.split('#') - const url = `https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/${providerId}/web?start=${date - .startOf('d') - .unix()}&duration=1440&channelSourceIds=${channelSourceIds}` - - return url - }, - async parser({ content }) { - const programs = [] - const items = parseItems(content) - for (let item of items) { - const details = await loadProgramDetails(item) - programs.push({ - title: item.title, - sub_title: details.episodeTitle, - description: details.description, - season: details.seasonNumber, - episode: details.episodeNumber, - rating: parseRating(item), - categories: parseCategories(details), - start: parseTime(item.startTime), - stop: parseTime(item.endTime) - }) - } - - return programs - } -} - -function parseRating(item) { - return item.rating ? { system: 'MPA', value: item.rating } : null -} - -function parseCategories(details) { - return Array.isArray(details.genres) ? details.genres.map(g => g.name) : [] -} - -function parseTime(timestamp) { - return dayjs.unix(timestamp) -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data.data || !Array.isArray(data.data.items) || !data.data.items.length) return [] - - return data.data.items[0].programSchedules -} - -async function loadProgramDetails(item) { - const data = await axios - .get(item.programDetails) - .then(r => r.data) - .catch(err => { - console.log(err.message) - }) - if (!data || !data.data || !data.data.item) return {} - - return data.data.item -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'tvguide.com', + days: 2, + url: function ({ date, channel }) { + const [providerId, channelSourceIds] = channel.site_id.split('#') + const url = `https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/${providerId}/web?start=${date + .startOf('d') + .unix()}&duration=1440&channelSourceIds=${channelSourceIds}` + + return url + }, + async parser({ content }) { + const programs = [] + const items = parseItems(content) + for (let item of items) { + const details = await loadProgramDetails(item) + programs.push({ + title: item.title, + sub_title: details.episodeTitle, + description: details.description, + season: details.seasonNumber, + episode: details.episodeNumber, + rating: parseRating(item), + categories: parseCategories(details), + start: parseTime(item.startTime), + stop: parseTime(item.endTime) + }) + } + + return programs + } +} + +function parseRating(item) { + return item.rating ? { system: 'MPA', value: item.rating } : null +} + +function parseCategories(details) { + return Array.isArray(details.genres) ? details.genres.map(g => g.name) : [] +} + +function parseTime(timestamp) { + return dayjs.unix(timestamp) +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data.data || !Array.isArray(data.data.items) || !data.data.items.length) return [] + + return data.data.items[0].programSchedules +} + +async function loadProgramDetails(item) { + const data = await axios + .get(item.programDetails) + .then(r => r.data) + .catch(err => { + console.log(err.message) + }) + if (!data || !data.data || !data.data.item) return {} + + return data.data.item +} diff --git a/sites/tvguide.com/tvguide.com.test.js b/sites/tvguide.com/tvguide.com.test.js index 3010d454..07538591 100644 --- a/sites/tvguide.com/tvguide.com.test.js +++ b/sites/tvguide.com/tvguide.com.test.js @@ -1,74 +1,74 @@ -// npm run grab -- --site=tvguide.com - -const { parser, url } = require('./tvguide.com.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '9100001138#9200018514', - xmltv_id: 'CBSEast.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/9100001138/web?start=1667088000&duration=1440&channelSourceIds=9200018514' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/programdetails/6060613824/web' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[5]).toMatchObject({ - start: '2022-10-30T13:00:00.000Z', - stop: '2022-10-30T14:30:00.000Z', - title: 'CBS Sunday Morning', - sub_title: '10-30-2022', - description: - 'The Backseat Lovers perform on the "Saturday Sessions"; and Daisy Ryan guests on "The Dish." Also: comedian Fortune Feimster.', - categories: ['Talk & Interview', 'Other'], - season: 40, - episode: 248, - rating: { - system: 'MPA', - value: 'TV-PG' - } - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - channel, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) - }) - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=tvguide.com + +const { parser, url } = require('./tvguide.com.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '9100001138#9200018514', + xmltv_id: 'CBSEast.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/9100001138/web?start=1667088000&duration=1440&channelSourceIds=9200018514' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://fandom-prod.apigee.net/v1/xapi/tvschedules/tvguide/programdetails/6060613824/web' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[5]).toMatchObject({ + start: '2022-10-30T13:00:00.000Z', + stop: '2022-10-30T14:30:00.000Z', + title: 'CBS Sunday Morning', + sub_title: '10-30-2022', + description: + 'The Backseat Lovers perform on the "Saturday Sessions"; and Daisy Ryan guests on "The Dish." Also: comedian Fortune Feimster.', + categories: ['Talk & Interview', 'Other'], + season: 40, + episode: 248, + rating: { + system: 'MPA', + value: 'TV-PG' + } + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + channel, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json')) + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js index adbb0c13..627b997c 100644 --- a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js +++ b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js @@ -1,113 +1,113 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvguide.myjcom.jp', - days: 2, - url: function ({ date, channel }) { - const id = `${channel.site_id}_${date.format('YYYYMMDD')}` - - return `https://tvguide.myjcom.jp/api/getEpgInfo/?channels=${id}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.commentary, - category: parseCategory(item), - icon: parseIcon(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const requests = [ - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=2&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=3&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=5&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=120&area=108&channelGenre&course&chart&is_adult=true' - ), - axios.get( - 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=200&area=108&channelGenre&course&chart&is_adult=true' - ) - ] - - let items = [] - await Promise.all(requests) - .then(responses => { - for (const r of responses) { - items = items.concat(r.data.header) - } - }) - .catch(console.log) - - return items.map(item => { - return { - lang: 'jp', - site_id: `${item.channel_type}_${item.channel_id}_${item.network_id}`, - name: item.channel_name - } - }) - } -} - -function parseIcon(item) { - return item.imgPath ? `https://tvguide.myjcom.jp${item.imgPath}` : null -} - -function parseCategory(item) { - if (!item.sortGenre) return null - - const id = item.sortGenre[0] - const genres = { - 0: 'ニュース/報道', - 1: 'スポーツ', - 2: '情報/ワイドショー', - 3: 'ドラマ', - 4: '音楽', - 5: 'バラエティ', - 6: '映画', - 7: 'アニメ/特撮', - 8: 'ドキュメンタリー/教養', - 9: '劇場/公演', - 10: '趣味/教育', - 11: '福祉', - 12: 'その他' - } - - return genres[id] -} - -function parseStart(item) { - return dayjs.tz(item.programStart.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') -} - -function parseStop(item) { - return dayjs.tz(item.programEnd.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') -} - -function parseItems(content, channel, date) { - const id = `${channel.site_id}_${date.format('YYYYMMDD')}` - const parsed = JSON.parse(content) - - return parsed[id] || [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvguide.myjcom.jp', + days: 2, + url: function ({ date, channel }) { + const id = `${channel.site_id}_${date.format('YYYYMMDD')}` + + return `https://tvguide.myjcom.jp/api/getEpgInfo/?channels=${id}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.commentary, + category: parseCategory(item), + icon: parseIcon(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const requests = [ + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=2&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=3&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=5&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=120&area=108&channelGenre&course&chart&is_adult=true' + ), + axios.get( + 'https://tvguide.myjcom.jp/api/mypage/getEpgChannelList/?channelType=200&area=108&channelGenre&course&chart&is_adult=true' + ) + ] + + let items = [] + await Promise.all(requests) + .then(responses => { + for (const r of responses) { + items = items.concat(r.data.header) + } + }) + .catch(console.log) + + return items.map(item => { + return { + lang: 'jp', + site_id: `${item.channel_type}_${item.channel_id}_${item.network_id}`, + name: item.channel_name + } + }) + } +} + +function parseIcon(item) { + return item.imgPath ? `https://tvguide.myjcom.jp${item.imgPath}` : null +} + +function parseCategory(item) { + if (!item.sortGenre) return null + + const id = item.sortGenre[0] + const genres = { + 0: 'ニュース/報道', + 1: 'スポーツ', + 2: '情報/ワイドショー', + 3: 'ドラマ', + 4: '音楽', + 5: 'バラエティ', + 6: '映画', + 7: 'アニメ/特撮', + 8: 'ドキュメンタリー/教養', + 9: '劇場/公演', + 10: '趣味/教育', + 11: '福祉', + 12: 'その他' + } + + return genres[id] +} + +function parseStart(item) { + return dayjs.tz(item.programStart.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') +} + +function parseStop(item) { + return dayjs.tz(item.programEnd.toString(), 'YYYYMMDDHHmmss', 'Asia/Tokyo') +} + +function parseItems(content, channel, date) { + const id = `${channel.site_id}_${date.format('YYYYMMDD')}` + const parsed = JSON.parse(content) + + return parsed[id] || [] +} diff --git a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js index 3f3b7c35..520d7a3a 100644 --- a/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js +++ b/sites/tvguide.myjcom.jp/tvguide.myjcom.jp.test.js @@ -1,52 +1,52 @@ -// npm run channels:parse -- --config=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js --output=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.channels.xml -// npm run grab -- --site=tvguide.myjcom.jp - -const { parser, url } = require('./tvguide.myjcom.jp.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-01-14', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '120_200_4', - name: 'Star Channel 1', - xmltv_id: 'StarChannel1.jp' -} -const content = - '{"120_200_4_20220114":[{"@search.score":1,"cid":"120_7305523","serviceCode":"200_4","channelName":"スターチャンネル1","digitalNo":195,"eventId":"181","title":"[5.1]フードロア:タマリンド","commentary":"HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)","attr":["5.1","hd","cp1"],"sortGenre":"31","hasImage":1,"imgPath":"/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg","isRecommended":null,"programStart":20220114050000,"programEnd":20220114060000,"programDate":20220114,"programId":568519,"start_time":"00","duration":60,"top":300,"end_time":"20220114060000","channel_type":"120","is_end":false,"show_remoterec":true}]}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-01-13T20:00:00.000Z', - stop: '2022-01-13T21:00:00.000Z', - title: '[5.1]フードロア:タマリンド', - description: - 'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)', - icon: 'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg', - category: 'ドラマ' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"120_200_3_20220114":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.config.js --output=./sites/tvguide.myjcom.jp/tvguide.myjcom.jp.channels.xml +// npm run grab -- --site=tvguide.myjcom.jp + +const { parser, url } = require('./tvguide.myjcom.jp.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-01-14', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '120_200_4', + name: 'Star Channel 1', + xmltv_id: 'StarChannel1.jp' +} +const content = + '{"120_200_4_20220114":[{"@search.score":1,"cid":"120_7305523","serviceCode":"200_4","channelName":"スターチャンネル1","digitalNo":195,"eventId":"181","title":"[5.1]フードロア:タマリンド","commentary":"HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)","attr":["5.1","hd","cp1"],"sortGenre":"31","hasImage":1,"imgPath":"/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg","isRecommended":null,"programStart":20220114050000,"programEnd":20220114060000,"programDate":20220114,"programId":568519,"start_time":"00","duration":60,"top":300,"end_time":"20220114060000","channel_type":"120","is_end":false,"show_remoterec":true}]}' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://tvguide.myjcom.jp/api/getEpgInfo/?channels=120_200_4_20220114') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-01-13T20:00:00.000Z', + stop: '2022-01-13T21:00:00.000Z', + title: '[5.1]フードロア:タマリンド', + description: + 'HBO(R)アジア製作。日本の齊藤工などアジアの監督が、各国の食をテーマに描いたアンソロジーシリーズ。(全8話)(19年 シンガポール 56分)', + icon: 'https://tvguide.myjcom.jp/monomedia/si/2022/20220114/7305523/image/7743d17b655b8d2274ca58b74f2f095c.jpg', + category: 'ドラマ' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"120_200_3_20220114":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvhebdo.com/tvhebdo.com.config.js b/sites/tvhebdo.com/tvhebdo.com.config.js index 28932384..17bb5586 100644 --- a/sites/tvhebdo.com/tvhebdo.com.config.js +++ b/sites/tvhebdo.com/tvhebdo.com.config.js @@ -1,94 +1,94 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'tvhebdo.com', - days: 2, - url: function ({ channel, date }) { - return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format( - 'YYYY-MM-DD' - )}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start < prev.start) { - start = start.plus({ days: 1 }) - } - prev.stop = start - } - let stop = start.plus({ minutes: 30 }) - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - let items = [] - const offsets = [ - 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360 - ] - for (let offset of offsets) { - const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00` - console.log(url) - const html = await axios - .get(url, { - headers: { - Cookie: - 'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006' - } - }) - .then(r => r.data) - .catch(console.error) - const $ = cheerio.load(html) - const rows = $('table.gr_row').toArray() - items = items.concat(rows) - } - - console.log(`Found ${items.length} channels`) - - return items.map(item => { - const $item = cheerio.load(item) - const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr( - 'alt' - ) - const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href') - const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null] - return { - lang: 'fr', - site_id, - name - } - }) - } -} - -function parseTitle($item) { - return $item('.titre').first().text().trim() -} - -function parseStart($item, date) { - const time = $item('.heure').text() - - return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { - zone: 'America/Toronto' - }).toUTC() -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $( - '#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]' - ).toArray() -} +const cheerio = require('cheerio') +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'tvhebdo.com', + days: 2, + url: function ({ channel, date }) { + return `https://www.tvhebdo.com/horaire-tele/${channel.site_id}/date/${date.format( + 'YYYY-MM-DD' + )}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start < prev.start) { + start = start.plus({ days: 1 }) + } + prev.stop = start + } + let stop = start.plus({ minutes: 30 }) + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + let items = [] + const offsets = [ + 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360 + ] + for (let offset of offsets) { + const url = `https://www.tvhebdo.com/horaire/gr/offset/${offset}/gr_id/0/date/2022-05-11/time/12:00:00` + console.log(url) + const html = await axios + .get(url, { + headers: { + Cookie: + 'distributeur=8004264; __utmz=222163677.1652094266.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _gcl_au=1.1.656635701.1652094273; tvh=3c2kaml9u14m83v91bg4dqgaf3; __utmc=222163677; IR_gbd=tvhebdo.com; IR_MPI=cf76b363-cf87-11ec-93f5-13daf79f8f76%7C1652367602625; __utma=222163677.2064368965.1652094266.1652281202.1652281479.3; __utmt=1; IR_MPS=1652284935955%7C1652284314367; _uetsid=0d8e2e60d13b11ec850db551304ae9e7; _uetvid=80456fa0b26e11ec9bf94951ce79b5f8; __utmb=222163677.19.9.1652284953979; __atuvc=30%7C19; __atuvs=627bdb98682bc242006' + } + }) + .then(r => r.data) + .catch(console.error) + const $ = cheerio.load(html) + const rows = $('table.gr_row').toArray() + items = items.concat(rows) + } + + console.log(`Found ${items.length} channels`) + + return items.map(item => { + const $item = cheerio.load(item) + const name = $item('.gr_row_head > div > a.gr_row_head_logo.link_to_station > img').attr( + 'alt' + ) + const url = $item('.gr_row_head > div > div.gr_row_head_poste > a').attr('href') + const [, site_id] = url.match(/horaire-tele\/(.*)/) || [null, null] + return { + lang: 'fr', + site_id, + name + } + }) + } +} + +function parseTitle($item) { + return $item('.titre').first().text().trim() +} + +function parseStart($item, date) { + const time = $item('.heure').text() + + return DateTime.fromFormat(`${date.format('YYYY-MM-DD')} ${time}`, 'yyyy-MM-dd HH:mm', { + zone: 'America/Toronto' + }).toUTC() +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $( + '#main_container > div.liste_container > table > tbody > tr[class^=liste_row_style_]' + ).toArray() +} diff --git a/sites/tvhebdo.com/tvhebdo.com.test.js b/sites/tvhebdo.com/tvhebdo.com.test.js index 960112b1..4bf559a2 100644 --- a/sites/tvhebdo.com/tvhebdo.com.test.js +++ b/sites/tvhebdo.com/tvhebdo.com.test.js @@ -1,56 +1,56 @@ -// npm run channels:parse -- --config=./sites/tvhebdo.com/tvhebdo.com.config.js --output=./sites/tvhebdo.com/tvhebdo.com.channels.xml -// npm run grab -- --site=tvhebdo.com - -const { parser, url } = require('./tvhebdo.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-05-11', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'src/CBFT', - xmltv_id: 'CBFT.ca' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvhebdo.com/horaire-tele/src/CBFT/date/2022-05-11' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-05-11T15:30:00.000Z', - stop: '2022-05-11T16:00:00.000Z', - title: '5 chefs dans ma cuisine' - }) - - expect(results[16]).toMatchObject({ - start: '2022-05-12T04:09:00.000Z', - stop: '2022-05-12T05:19:00.000Z', - title: 'Outlander: Le chardon et le tartan' - }) - - expect(results[36]).toMatchObject({ - start: '2022-05-12T15:00:00.000Z', - stop: '2022-05-12T15:30:00.000Z', - title: 'Ricardo' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/no_content.html')) - const result = parser({ content, date }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvhebdo.com/tvhebdo.com.config.js --output=./sites/tvhebdo.com/tvhebdo.com.channels.xml +// npm run grab -- --site=tvhebdo.com + +const { parser, url } = require('./tvhebdo.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-05-11', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'src/CBFT', + xmltv_id: 'CBFT.ca' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvhebdo.com/horaire-tele/src/CBFT/date/2022-05-11' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-05-11T15:30:00.000Z', + stop: '2022-05-11T16:00:00.000Z', + title: '5 chefs dans ma cuisine' + }) + + expect(results[16]).toMatchObject({ + start: '2022-05-12T04:09:00.000Z', + stop: '2022-05-12T05:19:00.000Z', + title: 'Outlander: Le chardon et le tartan' + }) + + expect(results[36]).toMatchObject({ + start: '2022-05-12T15:00:00.000Z', + stop: '2022-05-12T15:30:00.000Z', + title: 'Ricardo' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve('sites/tvhebdo.com/__data__/no_content.html')) + const result = parser({ content, date }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvheute.at/tvheute.at.config.js b/sites/tvheute.at/tvheute.at.config.js index f1796a36..500c91ba 100644 --- a/sites/tvheute.at/tvheute.at.config.js +++ b/sites/tvheute.at/tvheute.at.config.js @@ -1,73 +1,73 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvheute.at', - days: 2, - url({ channel, date }) { - return `https://tvheute.at/part/channel-shows/partial/${channel.site_id}/${date.format( - 'DD-MM-YYYY' - )}` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - icon: parseIcon(item), - category: parseCategory(item), - start: parseStart(item).toJSON(), - stop: parseStop(item).toJSON() - }) - }) - - return programs - } -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('.title-col strong').text() -} - -function parseDescription(item) { - const $ = cheerio.load(item) - - return $('.title-col .description').text() -} - -function parseCategory(item) { - const $ = cheerio.load(item) - - return $('.station-col > .type').text() -} - -function parseIcon(item) { - const $ = cheerio.load(item) - const imgSrc = $('.title-col .image img').data('src-desktop') - - return imgSrc ? `https://tvheute.at${imgSrc}` : null -} - -function parseStart(item) { - const $ = cheerio.load(item) - const time = $('.end-col > .duration-wrapper').data('start') - - return dayjs(time) -} - -function parseStop(item) { - const $ = cheerio.load(item) - const time = $('.end-col > .duration-wrapper').data('stop') - - return dayjs(time) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('#showListContainer > table > tbody > tr').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvheute.at', + days: 2, + url({ channel, date }) { + return `https://tvheute.at/part/channel-shows/partial/${channel.site_id}/${date.format( + 'DD-MM-YYYY' + )}` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + icon: parseIcon(item), + category: parseCategory(item), + start: parseStart(item).toJSON(), + stop: parseStop(item).toJSON() + }) + }) + + return programs + } +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('.title-col strong').text() +} + +function parseDescription(item) { + const $ = cheerio.load(item) + + return $('.title-col .description').text() +} + +function parseCategory(item) { + const $ = cheerio.load(item) + + return $('.station-col > .type').text() +} + +function parseIcon(item) { + const $ = cheerio.load(item) + const imgSrc = $('.title-col .image img').data('src-desktop') + + return imgSrc ? `https://tvheute.at${imgSrc}` : null +} + +function parseStart(item) { + const $ = cheerio.load(item) + const time = $('.end-col > .duration-wrapper').data('start') + + return dayjs(time) +} + +function parseStop(item) { + const $ = cheerio.load(item) + const time = $('.end-col > .duration-wrapper').data('stop') + + return dayjs(time) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('#showListContainer > table > tbody > tr').toArray() +} diff --git a/sites/tvheute.at/tvheute.at.test.js b/sites/tvheute.at/tvheute.at.test.js index c513936f..fb5905b1 100644 --- a/sites/tvheute.at/tvheute.at.test.js +++ b/sites/tvheute.at/tvheute.at.test.js @@ -1,50 +1,50 @@ -// npm run grab -- --site=tvheute.at - -const { parser, url } = require('./tvheute.at.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' } -const content = ` -

        Das ORF1 Programm mit allen Sendungen live im TV von tv.orf.at. Sie haben eine Sendung verpasst. In der ORF TVthek finden Sie viele Sendungen on demand zum Abruf als online Video und live stream.

        ORF1 heute

        SKYsp2 ORF2
        Sender Zeit Zeit Titel Start Titel
        ORF1 Kids
        Monchhichi (Wh.) ANIMATIONSSERIE Der Streiche-Wettbewerb
        Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.
        ORF1
        ZIB 18 NACHRICHTEN
        -` - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021' - ) -}) - -it('can parse response', () => { - expect(parser({ date, channel, content })).toMatchObject([ - { - start: '2021-11-08T05:00:00.000Z', - stop: '2021-11-08T05:10:00.000Z', - title: 'Monchhichi (Wh.)', - category: 'Kids', - description: - 'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.', - icon: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg' - }, - { - start: '2021-11-08T17:00:00.000Z', - stop: '2021-11-08T17:10:00.000Z', - title: 'ZIB 18' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: `Object moved -

        Object moved to here.

        -` - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvheute.at + +const { parser, url } = require('./tvheute.at.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-08', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'orf1', xmltv_id: 'ORF1.at' } +const content = ` +

        Das ORF1 Programm mit allen Sendungen live im TV von tv.orf.at. Sie haben eine Sendung verpasst. In der ORF TVthek finden Sie viele Sendungen on demand zum Abruf als online Video und live stream.

        ORF1 heute

        SKYsp2 ORF2
        Sender Zeit Zeit Titel Start Titel
        ORF1 Kids
        Monchhichi (Wh.) ANIMATIONSSERIE Der Streiche-Wettbewerb
        Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.
        ORF1
        ZIB 18 NACHRICHTEN
        +` + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tvheute.at/part/channel-shows/partial/orf1/08-11-2021' + ) +}) + +it('can parse response', () => { + expect(parser({ date, channel, content })).toMatchObject([ + { + start: '2021-11-08T05:00:00.000Z', + stop: '2021-11-08T05:10:00.000Z', + title: 'Monchhichi (Wh.)', + category: 'Kids', + description: + 'Roger hat sich Ärger mit Dr. Bellows eingehandelt, der ihn für einen Monat strafversetzen möchte. Einmal mehr hadert Roger mit dem Schicksal, dass er keinen eigenen Flaschengeist besitzt, der ihm aus der Patsche helfen kann. Jeannie schlägt vor, ihm Cousine Marilla zu schicken. Doch Tony ist strikt dagegen. Als ein Zaubererpärchen im exotischen Bühnenoutfit für die Zeit von Rogers Abwesenheit sein Apartment in Untermiete bezieht, glaubt Roger, Jeannie habe ihm ihre Verwandte doch noch gesandt.', + icon: 'https://tvheute.at/images/orf1/monchhichi_kids--1895216560-00.jpg' + }, + { + start: '2021-11-08T17:00:00.000Z', + stop: '2021-11-08T17:10:00.000Z', + title: 'ZIB 18' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: `Object moved +

        Object moved to here.

        +` + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvim.tv/tvim.tv.config.js b/sites/tvim.tv/tvim.tv.config.js index 9d0b4095..37fd3b04 100644 --- a/sites/tvim.tv/tvim.tv.config.js +++ b/sites/tvim.tv/tvim.tv.config.js @@ -1,43 +1,43 @@ -const dayjs = require('dayjs') - -module.exports = { - site: 'tvim.tv', - days: 2, - url: function ({ date, channel }) { - return `https://www.tvim.tv/script/program_epg?date=${date.format('DD.MM.YYYY')}&prog=${ - channel.site_id - }&server_time=true` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - - programs.push({ - title: item.title, - description: item.desc, - category: item.genre, - start: start.toString(), - stop: stop.toString() - }) - }) - - return programs - } -} - -function parseStart(item) { - return dayjs.unix(item.from_utc) -} - -function parseStop(item) { - return dayjs.unix(item.end_utc) -} - -function parseItems(content) { - const parsed = JSON.parse(content) - - return parsed.data.prog || [] -} +const dayjs = require('dayjs') + +module.exports = { + site: 'tvim.tv', + days: 2, + url: function ({ date, channel }) { + return `https://www.tvim.tv/script/program_epg?date=${date.format('DD.MM.YYYY')}&prog=${ + channel.site_id + }&server_time=true` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + + programs.push({ + title: item.title, + description: item.desc, + category: item.genre, + start: start.toString(), + stop: stop.toString() + }) + }) + + return programs + } +} + +function parseStart(item) { + return dayjs.unix(item.from_utc) +} + +function parseStop(item) { + return dayjs.unix(item.end_utc) +} + +function parseItems(content) { + const parsed = JSON.parse(content) + + return parsed.data.prog || [] +} diff --git a/sites/tvim.tv/tvim.tv.test.js b/sites/tvim.tv/tvim.tv.test.js index 5d639978..6d98e9d9 100644 --- a/sites/tvim.tv/tvim.tv.test.js +++ b/sites/tvim.tv/tvim.tv.test.js @@ -1,42 +1,42 @@ -// npm run grab -- --site=tvim.tv - -const { parser, url } = require('./tvim.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: 'T7', xmltv_id: 'T7.rs' } -const content = - '{"response":"ok","data":{"thumb":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_rel":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large_rel":"https://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","thumb_http":"http://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large":"http://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","server_time":1635100951,"catchup_length":2,"_id":"T73","ind":2,"genre":"national","name":"T7","epg_id":"T7","chan":"T7","prog":[{"id":"T7-1635026400","title":"Programi i T7","from":1635026400,"end":1635040800,"starting":"00:00","from_utc":1635026400,"end_utc":1635040800,"desc":"Programi i T7","genre":"test","chan":"T7","epg_id":"T7","eng":""}]}}' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe( - 'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true' - ) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: 'Sat, 23 Oct 2021 22:00:00 GMT', - stop: 'Sun, 24 Oct 2021 02:00:00 GMT', - title: 'Programi i T7', - description: 'Programi i T7', - category: 'test' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"response":"ok","data":{"server_time":1635100927}}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvim.tv + +const { parser, url } = require('./tvim.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: 'T7', xmltv_id: 'T7.rs' } +const content = + '{"response":"ok","data":{"thumb":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_rel":"https://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large_rel":"https://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","thumb_http":"http://mobile-api.tvim.tv/images/chan_logos/70x25/T7.png","thumb_large":"http://mobile-api.tvim.tv/images/chan_logos/120x60/T7.png","server_time":1635100951,"catchup_length":2,"_id":"T73","ind":2,"genre":"national","name":"T7","epg_id":"T7","chan":"T7","prog":[{"id":"T7-1635026400","title":"Programi i T7","from":1635026400,"end":1635040800,"starting":"00:00","from_utc":1635026400,"end_utc":1635040800,"desc":"Programi i T7","genre":"test","chan":"T7","epg_id":"T7","eng":""}]}}' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe( + 'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true' + ) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: 'Sat, 23 Oct 2021 22:00:00 GMT', + stop: 'Sun, 24 Oct 2021 02:00:00 GMT', + title: 'Programi i T7', + description: 'Programi i T7', + category: 'test' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"response":"ok","data":{"server_time":1635100927}}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tving.com/tving.com.config.js b/sites/tving.com/tving.com.config.js index 57cb04b4..7e9ce9dc 100644 --- a/sites/tving.com/tving.com.config.js +++ b/sites/tving.com/tving.com.config.js @@ -1,94 +1,94 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tving.com', - days: 2, - url: function ({ channel, date }) { - return `https://api.tving.com/v2/media/schedules/${channel.site_id}/${date.format( - 'YYYYMMDD' - )}?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.program.name.ko, - description: item.program.synopsis.ko, - categories: parseCategories(item), - date: item.program.product_year, - directors: item.program.director, - actors: item.program.actor, - start: parseStart(item), - stop: parseStop(item), - icon: parseIcon(item) - }) - }) - - return programs - }, - async channels() { - let items = await axios - .get('https://m.tving.com/guide/schedule.tving') - .then(r => r.data) - .then(html => { - let $ = cheerio.load(html) - - return $('ul.cb > li').toArray() - }) - .catch(console.log) - - return items.map(item => { - let $item = cheerio.load(item) - let [, site_id] = $item('a') - .attr('href') - .match(/\?id=(.*)/) || [null, null] - let name = $item('img').attr('alt') - - return { - lang: 'ko', - site_id, - name - } - }) - } -} - -function parseIcon(item) { - return item.program.image.length ? `https://image.tving.com${item.program.image[0].url}` : null -} - -function parseStart(item) { - return dayjs.tz(item.broadcast_start_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') -} - -function parseStop(item) { - return dayjs.tz(item.broadcast_end_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') -} - -function parseCategories(item) { - const categories = [] - - if (item.category1_name) categories.push(item.category1_name.ko) - if (item.category2_name) categories.push(item.category2_name.ko) - - return categories.filter(Boolean) -} - -function parseItems(content) { - let data = (content.match(/cb\((.*)\)/) || [null, null])[1] - if (!data) return [] - let json = JSON.parse(data) - if (!json || !json.body || !Array.isArray(json.body.result)) return [] - - return json.body.result -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tving.com', + days: 2, + url: function ({ channel, date }) { + return `https://api.tving.com/v2/media/schedules/${channel.site_id}/${date.format( + 'YYYYMMDD' + )}?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.program.name.ko, + description: item.program.synopsis.ko, + categories: parseCategories(item), + date: item.program.product_year, + directors: item.program.director, + actors: item.program.actor, + start: parseStart(item), + stop: parseStop(item), + icon: parseIcon(item) + }) + }) + + return programs + }, + async channels() { + let items = await axios + .get('https://m.tving.com/guide/schedule.tving') + .then(r => r.data) + .then(html => { + let $ = cheerio.load(html) + + return $('ul.cb > li').toArray() + }) + .catch(console.log) + + return items.map(item => { + let $item = cheerio.load(item) + let [, site_id] = $item('a') + .attr('href') + .match(/\?id=(.*)/) || [null, null] + let name = $item('img').attr('alt') + + return { + lang: 'ko', + site_id, + name + } + }) + } +} + +function parseIcon(item) { + return item.program.image.length ? `https://image.tving.com${item.program.image[0].url}` : null +} + +function parseStart(item) { + return dayjs.tz(item.broadcast_start_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') +} + +function parseStop(item) { + return dayjs.tz(item.broadcast_end_time.toString(), 'YYYYMMDDHHmmss', 'Asia/Seoul') +} + +function parseCategories(item) { + const categories = [] + + if (item.category1_name) categories.push(item.category1_name.ko) + if (item.category2_name) categories.push(item.category2_name.ko) + + return categories.filter(Boolean) +} + +function parseItems(content) { + let data = (content.match(/cb\((.*)\)/) || [null, null])[1] + if (!data) return [] + let json = JSON.parse(data) + if (!json || !json.body || !Array.isArray(json.body.result)) return [] + + return json.body.result +} diff --git a/sites/tving.com/tving.com.test.js b/sites/tving.com/tving.com.test.js index 31a9dd39..9f7cd6ce 100644 --- a/sites/tving.com/tving.com.test.js +++ b/sites/tving.com/tving.com.test.js @@ -1,50 +1,50 @@ -// npm run channels:parse -- --config=./sites/tving.com/tving.com.config.js --output=./sites/tving.com/tving.com.channels.xml -// npm run grab -- --site=tving.com - -const { parser, url } = require('./tving.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-23', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'C00551', - xmltv_id: 'tvN.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://api.tving.com/v2/media/schedules/C00551/20230123?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: '외계+인 1부', - description: '외계+인 1부', - icon: 'https://image.tving.com/upload/cms/caip/CAIP0200/P001661154.jpg', - date: 2022, - categories: [], - directors: ['최동훈'], - actors: ['김우빈', '류준열'], - start: '2023-01-22T13:40:00.000Z', - stop: '2023-01-22T15:00:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') - - expect(parser({ content })).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tving.com/tving.com.config.js --output=./sites/tving.com/tving.com.channels.xml +// npm run grab -- --site=tving.com + +const { parser, url } = require('./tving.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-23', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'C00551', + xmltv_id: 'tvN.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://api.tving.com/v2/media/schedules/C00551/20230123?callback=cb&pageNo=1&pageSize=500&screenCode=CSSD0200&networkCode=CSND0900&osCode=CSOD0900&teleCode=CSCD0900&apiKey=4263d7d76161f4a19a9efe9ca7903ec4' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: '외계+인 1부', + description: '외계+인 1부', + icon: 'https://image.tving.com/upload/cms/caip/CAIP0200/P001661154.jpg', + date: 2022, + categories: [], + directors: ['최동훈'], + actors: ['김우빈', '류준열'], + start: '2023-01-22T13:40:00.000Z', + stop: '2023-01-22T15:00:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') + + expect(parser({ content })).toMatchObject([]) +}) diff --git a/sites/tvmi.mt/tvmi.mt.config.js b/sites/tvmi.mt/tvmi.mt.config.js index 075f23b4..dc8bb8f6 100644 --- a/sites/tvmi.mt/tvmi.mt.config.js +++ b/sites/tvmi.mt/tvmi.mt.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') - -dayjs.extend(utc) -dayjs.extend(timezone) - -module.exports = { - site: 'tvmi.mt', - days: 2, - url: function ({ date, channel }) { - return `https://tvmi.mt/schedule/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('div > div:nth-child(2) > div:nth-child(2),a > div:nth-child(2) > div:nth-child(2)') - .text() - .trim() -} - -function parseDescription($item) { - return $item('div > div:nth-child(2) > div:nth-child(3),a > div:nth-child(2) > div:nth-child(3)') - .text() - .trim() -} - -function parseIcon($item) { - const bg = $item('div > div:nth-child(1) > div > div,a > div:nth-child(1) > div').data('bg') - - return bg ? `https:${bg}` : null -} - -function parseStart($item, date) { - const timeString = $item( - 'div > div:nth-child(2) > div:nth-child(1),a > div:nth-child(2) > div:nth-child(1)' - ) - .text() - .trim() - const [, HH, mm] = timeString.match(/^(\d{2}):(\d{2})/) || [null, null, null] - if (!HH || !mm) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Malta') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('body > main > div.mt-8 > div').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +module.exports = { + site: 'tvmi.mt', + days: 2, + url: function ({ date, channel }) { + return `https://tvmi.mt/schedule/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('div > div:nth-child(2) > div:nth-child(2),a > div:nth-child(2) > div:nth-child(2)') + .text() + .trim() +} + +function parseDescription($item) { + return $item('div > div:nth-child(2) > div:nth-child(3),a > div:nth-child(2) > div:nth-child(3)') + .text() + .trim() +} + +function parseIcon($item) { + const bg = $item('div > div:nth-child(1) > div > div,a > div:nth-child(1) > div').data('bg') + + return bg ? `https:${bg}` : null +} + +function parseStart($item, date) { + const timeString = $item( + 'div > div:nth-child(2) > div:nth-child(1),a > div:nth-child(2) > div:nth-child(1)' + ) + .text() + .trim() + const [, HH, mm] = timeString.match(/^(\d{2}):(\d{2})/) || [null, null, null] + if (!HH || !mm) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${HH}:${mm}`, 'YYYY-MM-DD HH:mm', 'Europe/Malta') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('body > main > div.mt-8 > div').toArray() +} diff --git a/sites/tvmi.mt/tvmi.mt.test.js b/sites/tvmi.mt/tvmi.mt.test.js index 675ff2c6..276ea896 100644 --- a/sites/tvmi.mt/tvmi.mt.test.js +++ b/sites/tvmi.mt/tvmi.mt.test.js @@ -1,53 +1,53 @@ -// npm run grab -- --site=tvmi.mt - -const { parser, url } = require('./tvmi.mt.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'TVM.mt' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe('https://tvmi.mt/schedule/2/2022-10-29') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - const results = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-29T03:30:00.000Z', - stop: '2022-10-29T04:00:00.000Z', - title: 'Bizzilla', - description: - 'Storja ta’ tliet familji, tnejn minnhom miżżewġin bejniethom, u familja oħra li għalkemm mhijiex, b’daqshekk ma jfissirx li mhijiex parti ntegrali fil-kompliċitá li ilha għaddejja bejniethom għal dawn l-aħħar tletin sena.', - icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/663927/image.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2022-10-29T04:00:00.000Z', - stop: '2022-10-29T04:30:00.000Z', - title: 'The Adventures of Puss in Boots', - icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/747336/image.jpg' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - channel - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvmi.mt + +const { parser, url } = require('./tvmi.mt.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-29', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'TVM.mt' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe('https://tvmi.mt/schedule/2/2022-10-29') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + const results = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-29T03:30:00.000Z', + stop: '2022-10-29T04:00:00.000Z', + title: 'Bizzilla', + description: + 'Storja ta’ tliet familji, tnejn minnhom miżżewġin bejniethom, u familja oħra li għalkemm mhijiex, b’daqshekk ma jfissirx li mhijiex parti ntegrali fil-kompliċitá li ilha għaddejja bejniethom għal dawn l-aħħar tletin sena.', + icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/663927/image.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2022-10-29T04:00:00.000Z', + stop: '2022-10-29T04:30:00.000Z', + title: 'The Adventures of Puss in Boots', + icon: 'https://dist4.tvmi.mt/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjMyNjEwNywiYXVkIjoiMTg4LjI0Mi40OC45MyIsImV4cCI6MTY2NzAxNjM1OH0.N4de761te_pRvWwSUnF6httRAzdukup5syejwXTUv8g/vod/747336/image.jpg' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvmusor.hu/tvmusor.hu.config.js b/sites/tvmusor.hu/tvmusor.hu.config.js index 9672ab61..373e4774 100644 --- a/sites/tvmusor.hu/tvmusor.hu.config.js +++ b/sites/tvmusor.hu/tvmusor.hu.config.js @@ -1,81 +1,81 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const _ = require('lodash') - -module.exports = { - site: 'tvmusor.hu', - days: 2, - url: 'http://www.tvmusor.hu/a/get-events/', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append( - 'data', - JSON.stringify({ - blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`] - }) - ) - - return params - } - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = dayjs(item.e) - let stop = dayjs(item.f) - if (prev) { - start = prev.stop - } - - programs.push({ - title: item.j, - category: item.h, - description: item.c, - icon: parseIcon(item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('http://www.tvmusor.hu/most/') - .then(r => r.data) - .catch(console.log) - - const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/) - const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":') - const channels = JSON.parse(json) - - return Object.values(channels).map(item => { - return { - lang: 'hu', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseIcon(item) { - return item.z ? `http://www.tvmusor.hu/images/events/408/${item.z}` : null -} - -function parseItems(content, channel, date) { - const data = JSON.parse(content) - if (!data || !data.data || !data.data.loadedBlocks) return [] - const blocks = data.data.loadedBlocks - const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}` - if (!Array.isArray(blocks[blockId])) return [] - - return _.uniqBy(_.uniqBy(blocks[blockId], 'e'), 'b') -} +const axios = require('axios') +const dayjs = require('dayjs') +const _ = require('lodash') + +module.exports = { + site: 'tvmusor.hu', + days: 2, + url: 'http://www.tvmusor.hu/a/get-events/', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append( + 'data', + JSON.stringify({ + blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`] + }) + ) + + return params + } + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = dayjs(item.e) + let stop = dayjs(item.f) + if (prev) { + start = prev.stop + } + + programs.push({ + title: item.j, + category: item.h, + description: item.c, + icon: parseIcon(item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('http://www.tvmusor.hu/most/') + .then(r => r.data) + .catch(console.log) + + const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/) + const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":') + const channels = JSON.parse(json) + + return Object.values(channels).map(item => { + return { + lang: 'hu', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseIcon(item) { + return item.z ? `http://www.tvmusor.hu/images/events/408/${item.z}` : null +} + +function parseItems(content, channel, date) { + const data = JSON.parse(content) + if (!data || !data.data || !data.data.loadedBlocks) return [] + const blocks = data.data.loadedBlocks + const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}` + if (!Array.isArray(blocks[blockId])) return [] + + return _.uniqBy(_.uniqBy(blocks[blockId], 'e'), 'b') +} diff --git a/sites/tvmusor.hu/tvmusor.hu.test.js b/sites/tvmusor.hu/tvmusor.hu.test.js index d04630d2..7aec3b64 100644 --- a/sites/tvmusor.hu/tvmusor.hu.test.js +++ b/sites/tvmusor.hu/tvmusor.hu.test.js @@ -1,71 +1,71 @@ -// npm run channels:parse -- --config=./sites/tvmusor.hu/tvmusor.hu.config.js --output=./sites/tvmusor.hu/tvmusor.hu.channels.xml -// npm run grab -- --site=tvmusor.hu - -const { parser, url, request } = require('./tvmusor.hu.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '290', - xmltv_id: 'M4Sport.hu' -} - -it('can generate valid url', () => { - expect(url).toBe('http://www.tvmusor.hu/a/get-events/') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ channel, date }) - expect(result.get('data')).toBe('{"blocks":["290|2022-11-19"]}') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-18T23:30:00.000Z', - stop: '2022-11-19T00:55:00.000Z', - title: 'Rövidpályás Úszó Országos Bajnokság', - category: 'sportműsor', - description: 'Forma-1 magazin. Hírek, információk, érdekességek a Forma-1 világából.', - icon: 'http://www.tvmusor.hu/images/events/408/f1e45193930943d9ee29769e0afa902aff0e4a90-better-call-saul.jpg' - }) - - expect(results[1]).toMatchObject({ - start: '2022-11-19T00:55:00.000Z', - stop: '2022-11-19T01:10:00.000Z', - title: 'Sportlövészet', - category: 'sportműsor' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"status":"error","reason":"invalid blocks"}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/tvmusor.hu/tvmusor.hu.config.js --output=./sites/tvmusor.hu/tvmusor.hu.channels.xml +// npm run grab -- --site=tvmusor.hu + +const { parser, url, request } = require('./tvmusor.hu.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-11-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '290', + xmltv_id: 'M4Sport.hu' +} + +it('can generate valid url', () => { + expect(url).toBe('http://www.tvmusor.hu/a/get-events/') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ channel, date }) + expect(result.get('data')).toBe('{"blocks":["290|2022-11-19"]}') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-18T23:30:00.000Z', + stop: '2022-11-19T00:55:00.000Z', + title: 'Rövidpályás Úszó Országos Bajnokság', + category: 'sportműsor', + description: 'Forma-1 magazin. Hírek, információk, érdekességek a Forma-1 világából.', + icon: 'http://www.tvmusor.hu/images/events/408/f1e45193930943d9ee29769e0afa902aff0e4a90-better-call-saul.jpg' + }) + + expect(results[1]).toMatchObject({ + start: '2022-11-19T00:55:00.000Z', + stop: '2022-11-19T01:10:00.000Z', + title: 'Sportlövészet', + category: 'sportműsor' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"status":"error","reason":"invalid blocks"}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvpassport.com/tvpassport.com.config.js b/sites/tvpassport.com/tvpassport.com.config.js index 2478da47..95b4ac22 100644 --- a/sites/tvpassport.com/tvpassport.com.config.js +++ b/sites/tvpassport.com/tvpassport.com.config.js @@ -1,156 +1,156 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvpassport.com', - days: 3, - url({ channel, date }) { - return `https://www.tvpassport.com/tv-listings/stations/${channel.site_id}/${date.format( - 'YYYY-MM-DD' - )}` - }, - request: { - headers: { - Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const $item = cheerio.load(item) - const start = parseStart($item) - const duration = parseDuration($item) - const stop = start.add(duration, 'm') - let title = parseTitle($item) - let sub_title = parseSubTitle($item) - if (title === 'Movie') { - title = sub_title - sub_title = null - } - - programs.push({ - title, - sub_title, - description: parseDescription($item), - icon: parseIcon($item), - category: parseCategory($item), - rating: parseRating($item), - actors: parseActors($item), - guest: parseGuest($item), - director: parseDirector($item), - start, - stop - }) - } - - return programs - }, - async channels() { - const content = await axios - .get('https://www.tvpassport.com/tv-listings', { - headers: { - Cookie: 'cisession=317b3a464bfe449650b7cc4b16ccf900a6646d88;' - } - }) - .then(r => r.data) - .catch(console.log) - const $ = cheerio.load(content) - - return $('.channel_cell') - .map((i, el) => { - const site_id = $(el) - .find('a') - .attr('href') - .replace('https://www.tvpassport.com/tv-listings/stations/', '') - const name = $(el).find('.sr-only').text().trim() - - return { - site_id, - name - } - }) - .get() - } -} - -function parseDescription($item) { - return $item('*').data('description') -} - -function parseIcon($item) { - const showpicture = $item('*').data('showpicture') - const url = new URL(showpicture, 'https://cdn.tvpassport.com/image/show/960x540/') - - return url.href -} - -function parseTitle($item) { - return $item('*').data('showname') -} - -function parseSubTitle($item) { - return $item('*').data('episodetitle') -} - -function parseCategory($item) { - const showtype = $item('*').data('showtype') - - return showtype ? showtype.split(', ') : [] -} - -function parseActors($item) { - const cast = $item('*').data('cast') - - return cast ? cast.split(', ') : [] -} - -function parseDirector($item) { - const director = $item('*').data('director') - - return director ? director.split(', ') : [] -} - -function parseGuest($item) { - const guest = $item('*').data('guest') - - return guest ? guest.split(', ') : [] -} - -function parseRating($item) { - const rating = $item('*').data('rating') - - return rating - ? { - system: 'MPA', - value: rating.replace(/^TV/, 'TV-') - } - : null -} - -function parseStart($item) { - const time = $item('*').data('st') - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'America/New_York') -} - -function parseDuration($item) { - const duration = $item('*').data('duration') - - return parseInt(duration) -} - -function parseItems(content) { - if (!content) return [] - const $ = cheerio.load(content) - - return $('.station-listings .list-group-item').toArray() -} +const axios = require('axios') +const dayjs = require('dayjs') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvpassport.com', + days: 3, + url({ channel, date }) { + return `https://www.tvpassport.com/tv-listings/stations/${channel.site_id}/${date.format( + 'YYYY-MM-DD' + )}` + }, + request: { + headers: { + Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const $item = cheerio.load(item) + const start = parseStart($item) + const duration = parseDuration($item) + const stop = start.add(duration, 'm') + let title = parseTitle($item) + let sub_title = parseSubTitle($item) + if (title === 'Movie') { + title = sub_title + sub_title = null + } + + programs.push({ + title, + sub_title, + description: parseDescription($item), + icon: parseIcon($item), + category: parseCategory($item), + rating: parseRating($item), + actors: parseActors($item), + guest: parseGuest($item), + director: parseDirector($item), + start, + stop + }) + } + + return programs + }, + async channels() { + const content = await axios + .get('https://www.tvpassport.com/tv-listings', { + headers: { + Cookie: 'cisession=317b3a464bfe449650b7cc4b16ccf900a6646d88;' + } + }) + .then(r => r.data) + .catch(console.log) + const $ = cheerio.load(content) + + return $('.channel_cell') + .map((i, el) => { + const site_id = $(el) + .find('a') + .attr('href') + .replace('https://www.tvpassport.com/tv-listings/stations/', '') + const name = $(el).find('.sr-only').text().trim() + + return { + site_id, + name + } + }) + .get() + } +} + +function parseDescription($item) { + return $item('*').data('description') +} + +function parseIcon($item) { + const showpicture = $item('*').data('showpicture') + const url = new URL(showpicture, 'https://cdn.tvpassport.com/image/show/960x540/') + + return url.href +} + +function parseTitle($item) { + return $item('*').data('showname') +} + +function parseSubTitle($item) { + return $item('*').data('episodetitle') +} + +function parseCategory($item) { + const showtype = $item('*').data('showtype') + + return showtype ? showtype.split(', ') : [] +} + +function parseActors($item) { + const cast = $item('*').data('cast') + + return cast ? cast.split(', ') : [] +} + +function parseDirector($item) { + const director = $item('*').data('director') + + return director ? director.split(', ') : [] +} + +function parseGuest($item) { + const guest = $item('*').data('guest') + + return guest ? guest.split(', ') : [] +} + +function parseRating($item) { + const rating = $item('*').data('rating') + + return rating + ? { + system: 'MPA', + value: rating.replace(/^TV/, 'TV-') + } + : null +} + +function parseStart($item) { + const time = $item('*').data('st') + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'America/New_York') +} + +function parseDuration($item) { + const duration = $item('*').data('duration') + + return parseInt(duration) +} + +function parseItems(content) { + if (!content) return [] + const $ = cheerio.load(content) + + return $('.station-listings .list-group-item').toArray() +} diff --git a/sites/tvpassport.com/tvpassport.com.test.js b/sites/tvpassport.com/tvpassport.com.test.js index 9e33be7d..6469f8ff 100644 --- a/sites/tvpassport.com/tvpassport.com.test.js +++ b/sites/tvpassport.com/tvpassport.com.test.js @@ -1,63 +1,63 @@ -// npm run grab -- --site=tvpassport.com -// npm run channels:parse -- --config=./sites/tvpassport.com/tvpassport.com.config.js --output=./sites/tvpassport.com/tvpassport.com.channels.xml - -const { parser, url, request } = require('./tvpassport.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'youtoo-america-network/5463', - xmltv_id: 'YTATV.us' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvpassport.com/tv-listings/stations/youtoo-america-network/5463/2022-10-04' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - - let results = parser({ content }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-04T10:00:00.000Z', - stop: '2022-10-04T10:30:00.000Z', - title: 'Charlie Moore: No Offense', - sub_title: 'Under the Influencer', - category: ['Sports', 'Outdoors'], - icon: 'https://cdn.tvpassport.com/image/show/960x540/69103.jpg', - rating: { - system: 'MPA', - value: 'TV-G' - }, - actors: ['John Reardon', 'Mayko Nguyen', 'Justin Kelly'], - director: ['Rob McElhenney'], - guest: ['Sean Penn'], - description: - 'Celebrity interviews while fishing in various locations throughout the United States.' - }) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvpassport.com +// npm run channels:parse -- --config=./sites/tvpassport.com/tvpassport.com.config.js --output=./sites/tvpassport.com/tvpassport.com.channels.xml + +const { parser, url, request } = require('./tvpassport.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'youtoo-america-network/5463', + xmltv_id: 'YTATV.us' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvpassport.com/tv-listings/stations/youtoo-america-network/5463/2022-10-04' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Cookie: 'cisession=e49ff13191d6875887193cae9e324b44ef85768d;' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + + let results = parser({ content }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-04T10:00:00.000Z', + stop: '2022-10-04T10:30:00.000Z', + title: 'Charlie Moore: No Offense', + sub_title: 'Under the Influencer', + category: ['Sports', 'Outdoors'], + icon: 'https://cdn.tvpassport.com/image/show/960x540/69103.jpg', + rating: { + system: 'MPA', + value: 'TV-G' + }, + actors: ['John Reardon', 'Mayko Nguyen', 'Justin Kelly'], + director: ['Rob McElhenney'], + guest: ['Sean Penn'], + description: + 'Celebrity interviews while fishing in various locations throughout the United States.' + }) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvplus.com.tr/tvplus.com.tr.config.js b/sites/tvplus.com.tr/tvplus.com.tr.config.js index 39e78a75..c460fd5d 100644 --- a/sites/tvplus.com.tr/tvplus.com.tr.config.js +++ b/sites/tvplus.com.tr/tvplus.com.tr.config.js @@ -1,74 +1,74 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'tvplus.com.tr', - days: 2, - url: 'https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList', - request: { - method: 'POST', - async headers() { - const response = await axios - .post('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/Authenticate', { - terminaltype: 'WEBTV_WIDEVINE', - userType: '3', - timezone: 'UTC' - }) - .catch(console.log) - const cookie = Array.isArray(response.headers['set-cookie']) - ? response.headers['set-cookie'].join('; ') - : '' - - return { cookie } - }, - data({ date, channel }) { - return { - type: '2', - channelid: channel.site_id, - begintime: date.format('YYYYMMDDHHmmss'), - endtime: date.add(1, 'd').format('YYYYMMDDHHmmss') - } - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const start = parseStart(item) - const stop = parseStop(item) - programs.push({ - title: item.name, - category: item.genres, - description: item.introduce, - icon: parseIcon(item), - start: start.toJSON(), - stop: stop.toJSON() - }) - }) - - return programs - } -} - -function parseIcon(item) { - return item.pictures && item.pictures.length ? item.pictures[0].href : null -} - -function parseStart(item) { - return dayjs.utc(item.starttime, 'YYYYMMDDHHmmss') -} - -function parseStop(item) { - return dayjs.utc(item.endtime, 'YYYYMMDDHHmmss') -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.playbilllist || [] -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'tvplus.com.tr', + days: 2, + url: 'https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList', + request: { + method: 'POST', + async headers() { + const response = await axios + .post('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/Authenticate', { + terminaltype: 'WEBTV_WIDEVINE', + userType: '3', + timezone: 'UTC' + }) + .catch(console.log) + const cookie = Array.isArray(response.headers['set-cookie']) + ? response.headers['set-cookie'].join('; ') + : '' + + return { cookie } + }, + data({ date, channel }) { + return { + type: '2', + channelid: channel.site_id, + begintime: date.format('YYYYMMDDHHmmss'), + endtime: date.add(1, 'd').format('YYYYMMDDHHmmss') + } + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const start = parseStart(item) + const stop = parseStop(item) + programs.push({ + title: item.name, + category: item.genres, + description: item.introduce, + icon: parseIcon(item), + start: start.toJSON(), + stop: stop.toJSON() + }) + }) + + return programs + } +} + +function parseIcon(item) { + return item.pictures && item.pictures.length ? item.pictures[0].href : null +} + +function parseStart(item) { + return dayjs.utc(item.starttime, 'YYYYMMDDHHmmss') +} + +function parseStop(item) { + return dayjs.utc(item.endtime, 'YYYYMMDDHHmmss') +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.playbilllist || [] +} diff --git a/sites/tvplus.com.tr/tvplus.com.tr.test.js b/sites/tvplus.com.tr/tvplus.com.tr.test.js index 820762c4..49019d13 100644 --- a/sites/tvplus.com.tr/tvplus.com.tr.test.js +++ b/sites/tvplus.com.tr/tvplus.com.tr.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=tvplus.com.tr - -const { parser, url, request } = require('./tvplus.com.tr.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '32', - xmltv_id: '24TV.tr' -} -const content = - '{"counttotal":"23","playbilllist":[{"country":"","starttime":"20211107000000","type":"PROGRAM","isBlackout":"0","rerun":"0","ppvsubscribed":"0","foreignsn":"134383557","isLive":"0","ratingid":"0","episodeTotalCount":"0","id":"134383557","keyword":"24 Portre","contentType":"0","isnpvr":"1","slsType":"0","iscpvr":"0","advisory":[],"genreIds":["1179"],"istvod":"0","name":"24 Portre","tvodStatus":"0","pictures":[{"href":"https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg","resolution":["null","null"],"imageType":"0"}],"externalContentCode":"105445035962202111070300","genres":"Yaşam","visittimes":"0","issubscribed":"0","programType":"program","gapFiller":"0","introduce":"Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.","priceType":[{"value":"0","key":"BTV"},{"value":"0","key":"TVOD"}],"endtime":"20211107010000","seasonTotalCount":"0","recordedMediaIds":[],"picture":{},"isLoyalty":"0","isppv":"0","mainGenre":"0","contentRight":"[{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"}]","channelid":"32"}],"playbillVersion":[{"channelId":"32","date":"20211108","version":"20211106000043"},{"channelId":"32","date":"20211107","version":"20211105000027"}]}' - -it('can generate valid url', () => { - expect(url).toBe('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList') -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result).toMatchObject({ - type: '2', - channelid: '32', - begintime: '20211107000000', - endtime: '20211108000000' - }) -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-07T00:00:00.000Z', - stop: '2021-11-07T01:00:00.000Z', - title: '24 Portre', - category: 'Yaşam', - icon: 'https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg', - description: - 'Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '{"counttotal":"0","playbilllist":[],"playbillVersion":[{"channelId":"10000","date":"20211108","version":"20211107163253"},{"channelId":"10000","date":"20211107","version":"20211107163253"}]}' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvplus.com.tr + +const { parser, url, request } = require('./tvplus.com.tr.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-07', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '32', + xmltv_id: '24TV.tr' +} +const content = + '{"counttotal":"23","playbilllist":[{"country":"","starttime":"20211107000000","type":"PROGRAM","isBlackout":"0","rerun":"0","ppvsubscribed":"0","foreignsn":"134383557","isLive":"0","ratingid":"0","episodeTotalCount":"0","id":"134383557","keyword":"24 Portre","contentType":"0","isnpvr":"1","slsType":"0","iscpvr":"0","advisory":[],"genreIds":["1179"],"istvod":"0","name":"24 Portre","tvodStatus":"0","pictures":[{"href":"https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg","resolution":["null","null"],"imageType":"0"}],"externalContentCode":"105445035962202111070300","genres":"Yaşam","visittimes":"0","issubscribed":"0","programType":"program","gapFiller":"0","introduce":"Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.","priceType":[{"value":"0","key":"BTV"},{"value":"0","key":"TVOD"}],"endtime":"20211107010000","seasonTotalCount":"0","recordedMediaIds":[],"picture":{},"isLoyalty":"0","isppv":"0","mainGenre":"0","contentRight":"[{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"3000435\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"13\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"14\\",\\"enable\\":\\"0\\"},{\\"mediaId\\":\\"100067919\\",\\"businessType\\":\\"15\\",\\"enable\\":\\"1\\"}]","channelid":"32"}],"playbillVersion":[{"channelId":"32","date":"20211108","version":"20211106000043"},{"channelId":"32","date":"20211107","version":"20211105000027"}]}' + +it('can generate valid url', () => { + expect(url).toBe('https://izmottvsc23.tvplus.com.tr:33207/EPG/JSON/PlayBillList') +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result).toMatchObject({ + type: '2', + channelid: '32', + begintime: '20211107000000', + endtime: '20211108000000' + }) +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-07T00:00:00.000Z', + stop: '2021-11-07T01:00:00.000Z', + title: '24 Portre', + category: 'Yaşam', + icon: 'https://izmottvsc23.tvplus.com.tr:33207/CPS/images/universal/film/program/202111/20211104/35/20211104000026695lh5.jpg', + description: + 'Kendi alanında büyük başarılar elde etmiş insanların kendi ağzından hayat hikayeleri ekrana geliyor.' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '{"counttotal":"0","playbilllist":[],"playbillVersion":[{"channelId":"10000","date":"20211108","version":"20211107163253"},{"channelId":"10000","date":"20211107","version":"20211107163253"}]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/tvprofil.com/tvprofil.com.config.js b/sites/tvprofil.com/tvprofil.com.config.js index 6cf2f80d..ac568528 100644 --- a/sites/tvprofil.com/tvprofil.com.config.js +++ b/sites/tvprofil.com/tvprofil.com.config.js @@ -1,97 +1,97 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') - -module.exports = { - site: 'tvprofil.com', - days: 2, - url: function ({ channel, date }) { - const parts = channel.site_id.split('#') - const query = buildQuery(parts[1], date) - - return `https://tvprofil.com/${parts[0]}/program/?${query}` - }, - request: { - headers: { - 'x-requested-with': 'XMLHttpRequest' - } - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const $item = cheerio.load(item) - const title = parseTitle($item) - const category = parseCategory($item) - const start = parseStart($item) - const duration = parseDuration($item) - const stop = start.add(duration, 's') - const icon = parseIcon($item) - - programs.push({ title, category, start, stop, icon }) - }) - - return programs - } -} - -function parseIcon($item) { - return $item(':root').data('image') -} - -function parseDuration($item) { - return $item(':root').data('len') -} - -function parseStart($item) { - const timestamp = $item(':root').data('ts') - - return dayjs.unix(timestamp) -} - -function parseCategory($item) { - return $item('.col:nth-child(2) > small').text() || null -} - -function parseTitle($item) { - let title = $item('.col:nth-child(2) > a').text() - title += $item('.col:nth-child(2)').clone().children().remove().end().text() - - return title.replace('®', '').trim().replace(/,$/, '') -} - -function parseItems(content) { - let data = (content.match(/cb\((.*)\)/) || [null, null])[1] - if (!data) return [] - let json = JSON.parse(data) - if (!json || !json.data || !json.data.program) return [] - - const $ = cheerio.load(json.data.program) - - return $('.row').toArray() -} - -function buildQuery(site_id, date) { - const query = { - datum: date.format('YYYY-MM-DD'), - kanal: site_id, - callback: 'cb' - } - - const a = query.datum + query.kanal - const ua = query.kanal + query.datum - - let i = a.length, - b = 2, - c = 2 - - for (var j = 0; j < ua.length; j++) c += ua.charCodeAt(j) - while (i--) { - b += (a.charCodeAt(i) + c * 2) * i - } - - const key = 'b' + b.toString().charCodeAt(2) - - query[key] = b - - return new URLSearchParams(query).toString() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') + +module.exports = { + site: 'tvprofil.com', + days: 2, + url: function ({ channel, date }) { + const parts = channel.site_id.split('#') + const query = buildQuery(parts[1], date) + + return `https://tvprofil.com/${parts[0]}/program/?${query}` + }, + request: { + headers: { + 'x-requested-with': 'XMLHttpRequest' + } + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const $item = cheerio.load(item) + const title = parseTitle($item) + const category = parseCategory($item) + const start = parseStart($item) + const duration = parseDuration($item) + const stop = start.add(duration, 's') + const icon = parseIcon($item) + + programs.push({ title, category, start, stop, icon }) + }) + + return programs + } +} + +function parseIcon($item) { + return $item(':root').data('image') +} + +function parseDuration($item) { + return $item(':root').data('len') +} + +function parseStart($item) { + const timestamp = $item(':root').data('ts') + + return dayjs.unix(timestamp) +} + +function parseCategory($item) { + return $item('.col:nth-child(2) > small').text() || null +} + +function parseTitle($item) { + let title = $item('.col:nth-child(2) > a').text() + title += $item('.col:nth-child(2)').clone().children().remove().end().text() + + return title.replace('®', '').trim().replace(/,$/, '') +} + +function parseItems(content) { + let data = (content.match(/cb\((.*)\)/) || [null, null])[1] + if (!data) return [] + let json = JSON.parse(data) + if (!json || !json.data || !json.data.program) return [] + + const $ = cheerio.load(json.data.program) + + return $('.row').toArray() +} + +function buildQuery(site_id, date) { + const query = { + datum: date.format('YYYY-MM-DD'), + kanal: site_id, + callback: 'cb' + } + + const a = query.datum + query.kanal + const ua = query.kanal + query.datum + + let i = a.length, + b = 2, + c = 2 + + for (var j = 0; j < ua.length; j++) c += ua.charCodeAt(j) + while (i--) { + b += (a.charCodeAt(i) + c * 2) * i + } + + const key = 'b' + b.toString().charCodeAt(2) + + query[key] = b + + return new URLSearchParams(query).toString() +} diff --git a/sites/tvprofil.com/tvprofil.com.test.js b/sites/tvprofil.com/tvprofil.com.test.js index 47af3e75..3019ab32 100644 --- a/sites/tvprofil.com/tvprofil.com.test.js +++ b/sites/tvprofil.com/tvprofil.com.test.js @@ -1,49 +1,49 @@ -// npm run grab -- --site=tvprofil.com - -const { parser, url, request } = require('./tvprofil.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-12', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'bg/tv-programa#24kitchen-bg', - xmltv_id: '24KitchenBulgaria.bg' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://tvprofil.com/bg/tv-programa/program/?datum=2023-01-12&kanal=24kitchen-bg&callback=cb&b55=747917' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Мексиканска кухня с Пати 10, еп. 9', - start: '2023-01-12T04:00:00.000Z', - stop: '2023-01-12T04:30:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') - - expect(parser({ content })).toMatchObject([]) -}) +// npm run grab -- --site=tvprofil.com + +const { parser, url, request } = require('./tvprofil.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-12', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'bg/tv-programa#24kitchen-bg', + xmltv_id: '24KitchenBulgaria.bg' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tvprofil.com/bg/tv-programa/program/?datum=2023-01-12&kanal=24kitchen-bg&callback=cb&b55=747917' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.txt'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Мексиканска кухня с Пати 10, еп. 9', + start: '2023-01-12T04:00:00.000Z', + stop: '2023-01-12T04:30:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.txt'), 'utf8') + + expect(parser({ content })).toMatchObject([]) +}) diff --git a/sites/tvtv.us/tvtv.us.config.js b/sites/tvtv.us/tvtv.us.config.js index 81763555..2f615763 100644 --- a/sites/tvtv.us/tvtv.us.config.js +++ b/sites/tvtv.us/tvtv.us.config.js @@ -1,40 +1,40 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') - -dayjs.extend(utc) - -module.exports = { - site: 'tvtv.us', - request: { - delay: 500 // 500 ms - }, - days: 2, - url: function ({ date, channel }) { - return `https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/${date.toJSON()}/${date - .add(1, 'd') - .toJSON()}/${channel.site_id}` - }, - parser: function ({ content }) { - let programs = [] - - const items = parseItems(content) - items.forEach(item => { - const start = dayjs.utc(item.startTime) - const stop = start.add(item.runTime, 'm') - programs.push({ - title: item.title, - description: item.subtitle, - start, - stop - }) - }) - - return programs - } -} - -function parseItems(content) { - const json = JSON.parse(content) - if (!json.length) return [] - return json[0] -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') + +dayjs.extend(utc) + +module.exports = { + site: 'tvtv.us', + request: { + delay: 500 // 500 ms + }, + days: 2, + url: function ({ date, channel }) { + return `https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/${date.toJSON()}/${date + .add(1, 'd') + .toJSON()}/${channel.site_id}` + }, + parser: function ({ content }) { + let programs = [] + + const items = parseItems(content) + items.forEach(item => { + const start = dayjs.utc(item.startTime) + const stop = start.add(item.runTime, 'm') + programs.push({ + title: item.title, + description: item.subtitle, + start, + stop + }) + }) + + return programs + } +} + +function parseItems(content) { + const json = JSON.parse(content) + if (!json.length) return [] + return json[0] +} diff --git a/sites/tvtv.us/tvtv.us.test.js b/sites/tvtv.us/tvtv.us.test.js index c22d6e45..685f617c 100644 --- a/sites/tvtv.us/tvtv.us.test.js +++ b/sites/tvtv.us/tvtv.us.test.js @@ -1,54 +1,54 @@ -// npm run grab -- --site=tvtv.us - -const { parser, url } = require('./tvtv.us.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-09-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '62670', - xmltv_id: 'AMITV.ca', - logo: 'https://tvtv.us/gn/i/assets/s62670_ll_h15_ab.png?w=360&h=270' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/2022-09-20T00:00:00.000Z/2022-09-21T00:00:00.000Z/62670' - ) -}) - -it('can parse response', () => { - const content = - '[[{"programId":"EP039131940001","title":"Beyond the Field","subtitle":"Diversity in Sport","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:00Z","start":0,"duration":30,"runTime":30},{"programId":"EP032368970002","title":"IGotThis","subtitle":"Listen to Dis","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:30Z","start":120,"duration":30,"runTime":30}]]' - - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-09-20T00:00:00.000Z', - stop: '2022-09-20T00:30:00.000Z', - title: 'Beyond the Field', - description: 'Diversity in Sport' - }, - { - start: '2022-09-20T00:30:00.000Z', - stop: '2022-09-20T01:00:00.000Z', - title: 'IGotThis', - description: 'Listen to Dis' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '[]' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=tvtv.us + +const { parser, url } = require('./tvtv.us.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-09-20', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '62670', + xmltv_id: 'AMITV.ca', + logo: 'https://tvtv.us/gn/i/assets/s62670_ll_h15_ab.png?w=360&h=270' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-DEFAULT/grid/2022-09-20T00:00:00.000Z/2022-09-21T00:00:00.000Z/62670' + ) +}) + +it('can parse response', () => { + const content = + '[[{"programId":"EP039131940001","title":"Beyond the Field","subtitle":"Diversity in Sport","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:00Z","start":0,"duration":30,"runTime":30},{"programId":"EP032368970002","title":"IGotThis","subtitle":"Listen to Dis","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:30Z","start":120,"duration":30,"runTime":30}]]' + + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-09-20T00:00:00.000Z', + stop: '2022-09-20T00:30:00.000Z', + title: 'Beyond the Field', + description: 'Diversity in Sport' + }, + { + start: '2022-09-20T00:30:00.000Z', + stop: '2022-09-20T01:00:00.000Z', + title: 'IGotThis', + description: 'Listen to Dis' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '[]' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/unifi.com.my/unifi.com.my.config.js b/sites/unifi.com.my/unifi.com.my.config.js index fb98d5ee..0996bb23 100644 --- a/sites/unifi.com.my/unifi.com.my.config.js +++ b/sites/unifi.com.my/unifi.com.my.config.js @@ -1,60 +1,60 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'unifi.com.my', - days: 2, - url: 'https://unifi.com.my/tv/api/tv', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - }, - method: 'POST', - headers: { - 'x-requested-with': 'XMLHttpRequest' - }, - data({ date }) { - const params = new URLSearchParams() - params.append('date', date.format('YYYY-MM-DD')) - return params - } - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const start = parseStart(item, date) - const stop = start.add(item.minute, 'minute') - programs.push({ - title: item.name, - start, - stop - }) - }) - return programs - } -} - -function parseItems(content, channel) { - try { - const data = JSON.parse(content) - if (!data) return [] - if (!Array.isArray(data)) return [] - - const channelData = data.find(i => i.id == channel.site_id) - return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] - } catch (err) { - return [] - } -} - -function parseStart(item, date) { - const time = `${date.format('YYYY-MM-DD')} ${item.start_time}` - return dayjs.tz(time, 'YYYY-MM-DD H:mma', 'Asia/Kuala_Lumpur') -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'unifi.com.my', + days: 2, + url: 'https://unifi.com.my/tv/api/tv', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + }, + method: 'POST', + headers: { + 'x-requested-with': 'XMLHttpRequest' + }, + data({ date }) { + const params = new URLSearchParams() + params.append('date', date.format('YYYY-MM-DD')) + return params + } + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const start = parseStart(item, date) + const stop = start.add(item.minute, 'minute') + programs.push({ + title: item.name, + start, + stop + }) + }) + return programs + } +} + +function parseItems(content, channel) { + try { + const data = JSON.parse(content) + if (!data) return [] + if (!Array.isArray(data)) return [] + + const channelData = data.find(i => i.id == channel.site_id) + return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] + } catch (err) { + return [] + } +} + +function parseStart(item, date) { + const time = `${date.format('YYYY-MM-DD')} ${item.start_time}` + return dayjs.tz(time, 'YYYY-MM-DD H:mma', 'Asia/Kuala_Lumpur') +} diff --git a/sites/unifi.com.my/unifi.com.my.test.js b/sites/unifi.com.my/unifi.com.my.test.js index 1ee1deb8..276ea0f6 100644 --- a/sites/unifi.com.my/unifi.com.my.test.js +++ b/sites/unifi.com.my/unifi.com.my.test.js @@ -1,57 +1,57 @@ -// npm run grab -- --site=unifi.com.my - -const { parser, url, request } = require('./unifi.com.my.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-13', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '20000009', - xmltv_id: 'TV1.my' -} - -it('can generate valid url', () => { - expect(url).toBe('https://unifi.com.my/tv/api/tv') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'x-requested-with': 'XMLHttpRequest' - }) -}) - -it('can generate valid request data', () => { - const data = request.data({ date }) - - expect(data.get('date')).toBe('2023-01-13') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - title: 'Berita Tengah Malam', - start: '2023-01-12T16:00:00.000Z', - stop: '2023-01-12T16:30:00.000Z' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ content: '', channel }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=unifi.com.my + +const { parser, url, request } = require('./unifi.com.my.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-13', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '20000009', + xmltv_id: 'TV1.my' +} + +it('can generate valid url', () => { + expect(url).toBe('https://unifi.com.my/tv/api/tv') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'x-requested-with': 'XMLHttpRequest' + }) +}) + +it('can generate valid request data', () => { + const data = request.data({ date }) + + expect(data.get('date')).toBe('2023-01-13') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + title: 'Berita Tengah Malam', + start: '2023-01-12T16:00:00.000Z', + stop: '2023-01-12T16:30:00.000Z' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ content: '', channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/vidio.com/vidio.com.config.js b/sites/vidio.com/vidio.com.config.js index 6312ea4e..3923d409 100644 --- a/sites/vidio.com/vidio.com.config.js +++ b/sites/vidio.com/vidio.com.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const { DateTime } = require('luxon') - -module.exports = { - site: 'vidio.com', - days: 2, - url({ channel }) { - return `https://www.vidio.com/live/${channel.site_id}/schedules` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev && start < prev.start) { - start = start.plus({ days: 1 }) - date = date.add(1, 'd') - } - let stop = parseStop($item, date) - if (stop < start) { - stop = stop.plus({ days: 1 }) - date = date.add(1, 'd') - } - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - } -} - -function parseStart($item, date) { - const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() - const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${start}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() -} - -function parseStop($item, date) { - const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() - const [, stop] = timeString.match(/- (\d{2}:\d{2}) WIB/) || [null, null] - const dateString = `${date.format('YYYY-MM-DD')} ${stop}` - - return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() -} - -function parseTitle($item) { - return $item('div.b-livestreaming-daily-schedule__item-content-title').text() -} - -function parseItems(content, date) { - const $ = cheerio.load(content) - - return $( - `#schedule-content-${date.format( - 'YYYYMMDD' - )} > .b-livestreaming-daily-schedule__scroll-container .b-livestreaming-daily-schedule__item` - ).toArray() -} +const cheerio = require('cheerio') +const { DateTime } = require('luxon') + +module.exports = { + site: 'vidio.com', + days: 2, + url({ channel }) { + return `https://www.vidio.com/live/${channel.site_id}/schedules` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev && start < prev.start) { + start = start.plus({ days: 1 }) + date = date.add(1, 'd') + } + let stop = parseStop($item, date) + if (stop < start) { + stop = stop.plus({ days: 1 }) + date = date.add(1, 'd') + } + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + } +} + +function parseStart($item, date) { + const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() + const [, start] = timeString.match(/(\d{2}:\d{2}) -/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${start}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() +} + +function parseStop($item, date) { + const timeString = $item('div.b-livestreaming-daily-schedule__item-content-caption').text() + const [, stop] = timeString.match(/- (\d{2}:\d{2}) WIB/) || [null, null] + const dateString = `${date.format('YYYY-MM-DD')} ${stop}` + + return DateTime.fromFormat(dateString, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Jakarta' }).toUTC() +} + +function parseTitle($item) { + return $item('div.b-livestreaming-daily-schedule__item-content-title').text() +} + +function parseItems(content, date) { + const $ = cheerio.load(content) + + return $( + `#schedule-content-${date.format( + 'YYYYMMDD' + )} > .b-livestreaming-daily-schedule__scroll-container .b-livestreaming-daily-schedule__item` + ).toArray() +} diff --git a/sites/vidio.com/vidio.com.test.js b/sites/vidio.com/vidio.com.test.js index 2f8e7bb4..ef5bec17 100644 --- a/sites/vidio.com/vidio.com.test.js +++ b/sites/vidio.com/vidio.com.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=vidio.com - -const { parser, url } = require('./vidio.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '7464', - xmltv_id: 'AjwaTV.id' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.vidio.com/live/7464/schedules') -}) - -it('can parse response', () => { - const content = - '' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-23T17:30:00.000Z', - stop: '2021-11-23T18:30:00.000Z', - title: '30 Hari 30 Juz' - }, - { - start: '2021-11-23T18:30:00.000Z', - stop: '2021-11-23T21:00:00.000Z', - title: 'Makkah Live' - }, - { - start: '2021-11-24T15:30:00.000Z', - stop: '2021-11-24T17:30:00.000Z', - title: 'FTV Islami' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=vidio.com + +const { parser, url } = require('./vidio.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-24', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '7464', + xmltv_id: 'AjwaTV.id' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.vidio.com/live/7464/schedules') +}) + +it('can parse response', () => { + const content = + '' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-23T17:30:00.000Z', + stop: '2021-11-23T18:30:00.000Z', + title: '30 Hari 30 Juz' + }, + { + start: '2021-11-23T18:30:00.000Z', + stop: '2021-11-23T21:00:00.000Z', + title: 'Makkah Live' + }, + { + start: '2021-11-24T15:30:00.000Z', + stop: '2021-11-24T17:30:00.000Z', + title: 'FTV Islami' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/virginmedia.com/virginmedia.com.config.js b/sites/virginmedia.com/virginmedia.com.config.js index c839f2bf..90f1c419 100644 --- a/sites/virginmedia.com/virginmedia.com.config.js +++ b/sites/virginmedia.com/virginmedia.com.config.js @@ -1,112 +1,112 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web' - -module.exports = { - site: 'virginmedia.com', - days: 2, - url: function ({ date }) { - return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const promises = [ - axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), - axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) - ] - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - }) - .catch(console.error) - // items.forEach(item => { - for (let item of items) { - const detail = await loadProgramDetails(item) - programs.push({ - title: item.t, - description: parseDescription(detail), - category: parseCategory(detail), - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - //) - - return programs - }, - async channels() { - const data = await axios - .get(`${API_ENDPOINT}/channels`) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'be', - site_id: item.id.replace('lgi-gb-prodobo-master:40980-', ''), - name: item.title - } - }) - } -} - -async function loadProgramDetails(item) { - if (!item.i) return {} - const url = `${API_ENDPOINT}/listings/${item.i}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - return data || {} -} - -function parseStart(item) { - return dayjs(item.s) -} - -function parseStop(item) { - return dayjs(item.e) -} - -function parseItems(content, channel) { - const data = typeof content === 'string' ? JSON.parse(content) : content - if (!data || !Array.isArray(data.entries)) return [] - const entity = data.entries.find(e => e.o === `lgi-gb-prodobo-master:${channel.site_id}`) - - return entity ? entity.l : [] -} - -function parseDescription(detail) { - return detail.program.longDescription || null -} - -function parseCategory(detail) { - let categories = [] - detail.program.categories.forEach(category => { - categories.push(category.title) - }) - return categories -} - -function parseSeason(detail) { - if (!detail.program.seriesNumber) return null - if (String(detail.program.seriesNumber).length > 2) return null - return detail.program.seriesNumber -} - -function parseEpisode(detail) { - if (!detail.program.seriesEpisodeNumber) return null - if (String(detail.program.seriesEpisodeNumber).length > 3) return null - return detail.program.seriesEpisodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web' + +module.exports = { + site: 'virginmedia.com', + days: 2, + url: function ({ date }) { + return `${API_ENDPOINT}/programschedules/${date.format('YYYYMMDD')}/1` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const promises = [ + axios.get(`${API_ENDPOINT}/programschedules/${d}/2`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/3`), + axios.get(`${API_ENDPOINT}/programschedules/${d}/4`) + ] + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + }) + .catch(console.error) + // items.forEach(item => { + for (let item of items) { + const detail = await loadProgramDetails(item) + programs.push({ + title: item.t, + description: parseDescription(detail), + category: parseCategory(detail), + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + //) + + return programs + }, + async channels() { + const data = await axios + .get(`${API_ENDPOINT}/channels`) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'be', + site_id: item.id.replace('lgi-gb-prodobo-master:40980-', ''), + name: item.title + } + }) + } +} + +async function loadProgramDetails(item) { + if (!item.i) return {} + const url = `${API_ENDPOINT}/listings/${item.i}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + return data || {} +} + +function parseStart(item) { + return dayjs(item.s) +} + +function parseStop(item) { + return dayjs(item.e) +} + +function parseItems(content, channel) { + const data = typeof content === 'string' ? JSON.parse(content) : content + if (!data || !Array.isArray(data.entries)) return [] + const entity = data.entries.find(e => e.o === `lgi-gb-prodobo-master:${channel.site_id}`) + + return entity ? entity.l : [] +} + +function parseDescription(detail) { + return detail.program.longDescription || null +} + +function parseCategory(detail) { + let categories = [] + detail.program.categories.forEach(category => { + categories.push(category.title) + }) + return categories +} + +function parseSeason(detail) { + if (!detail.program.seriesNumber) return null + if (String(detail.program.seriesNumber).length > 2) return null + return detail.program.seriesNumber +} + +function parseEpisode(detail) { + if (!detail.program.seriesEpisodeNumber) return null + if (String(detail.program.seriesEpisodeNumber).length > 3) return null + return detail.program.seriesEpisodeNumber +} diff --git a/sites/virginmedia.com/virginmedia.com.test.js b/sites/virginmedia.com/virginmedia.com.test.js index c8744329..ab6728f0 100644 --- a/sites/virginmedia.com/virginmedia.com.test.js +++ b/sites/virginmedia.com/virginmedia.com.test.js @@ -1,156 +1,156 @@ -// npm run channels:parse -- --config=./sites/virginmedia.com/virginmedia.com.config.js --output=./sites/virginmedia.com/virginmedia.com.channels.xml -// npm run grab -- --site=virginmedia.com - -const { parser, url } = require('./virginmedia.com.config.js') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-03-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '1761', - xmltv_id: 'ViaplaySports1.uk' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/1' - ) -}) - -it('can parse response', done => { - const content = - '{"entryCount":410,"totalResults":410,"updated":1647459686755,"expires":1647460298218,"title":"EPG","periods":4,"periodStartTime":1647475200000,"periodEndTime":1647496800000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","t":"Live: NHL Hockey","s":1647473400000,"e":1647484200000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - - axios.get.mockImplementation(url => { - if ( - url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/2' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":410,"totalResults":410,"updated":1647460887411,"expires":1647461895572,"title":"EPG","periods":4,"periodStartTime":1647496800000,"periodEndTime":1647518400000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","t":"Challenge Cup Ice Hockey","s":1647484200000,"e":1647496800000,"c":"lgi-gb-prodobo-master:genre-123","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/3' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461910282,"title":"EPG","periods":4,"periodStartTime":1647518400000,"periodEndTime":1647540000000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","t":"NHL Hockey","s":1647511200000,"e":1647518400000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/4' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461920720,"title":"EPG","periods":4,"periodStartTime":1647540000000,"periodEndTime":1647561600000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","t":"Boxing World Weekly","s":1647539100000,"e":1647540900000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","startTime":1647473400000,"endTime":1647484200000,"actualStartTime":1647473400000,"actualEndTime":1647484200000,"expirationDate":1648078200000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:de610af9a9b049c8a0245173f273136d36458f6f","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005","title":"Live: NHL Hockey","description":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","longDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_h8_aa.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","shortDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"20120000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Boston Bruins at Minnesota Wild"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647484200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763419|en-GB"}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","startTime":1647484200000,"endTime":1647496800000,"actualStartTime":1647484200000,"actualEndTime":1647496800000,"expirationDate":1648089000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005","title":"Challenge Cup Ice Hockey","description":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","longDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p11743980_b_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","shortDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"79870000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Final: Belfast Giants v Cardiff Devils"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647928800000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21720572|en-GB"}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","startTime":1647511200000,"endTime":1647518400000,"actualStartTime":1647511200000,"actualEndTime":1647518400000,"expirationDate":1648116000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435","title":"NHL Hockey","description":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","longDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_h8_ak.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_v12_ak.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","shortDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"194","seriesNumber":"102022","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"New Jersey Devils at Calgary Flames"},"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647583200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763550|en-GB"}' - ) - }) - } else if ( - url === - 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489' - ) { - return Promise.resolve({ - data: JSON.parse( - '{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","startTime":1647539100000,"endTime":1647540900000,"actualStartTime":1647539100000,"actualEndTime":1647540900000,"expirationDate":1648143900000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:c02da14358110cec07d14dc154717ce62ba2f489","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145","title":"Boxing World Weekly","description":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","longDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-83","title":"Boxing","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p19340143_b_v8_aa.jpg"},{"assetType":"TitleTreatment","assetTypes":["TitleTreatment"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p13641079_ttl_h95_aa.png"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","shortDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"60","seriesNumber":"4","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1648142400000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21764379|en-GB"}' - ) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - parser({ content, channel, date }) - .then(result => { - result = result.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-03-16T23:30:00.000Z', - stop: '2022-03-17T02:30:00.000Z', - title: 'Live: NHL Hockey', - description: - 'The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.', - category: ['Sport', 'Ice Hockey'] - }, - { - start: '2022-03-17T02:30:00.000Z', - stop: '2022-03-17T06:00:00.000Z', - title: 'Challenge Cup Ice Hockey', - description: - 'Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.', - category: ['Ice Hockey'] - }, - { - start: '2022-03-17T10:00:00.000Z', - stop: '2022-03-17T12:00:00.000Z', - title: 'NHL Hockey', - description: - 'The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.', - category: ['Sport', 'Ice Hockey'] - }, - { - start: '2022-03-17T17:45:00.000Z', - stop: '2022-03-17T18:15:00.000Z', - title: 'Boxing World Weekly', - description: - 'A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.', - category: ['Sport', 'Boxing'], - season: '4', - episode: '60' - } - ]) - done() - }) - .catch(done) -}) - -it('can handle empty guide', done => { - parser({ - content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', - channel, - date - }) - .then(result => { - expect(result).toMatchObject([]) - done() - }) - .catch(done) -}) +// npm run channels:parse -- --config=./sites/virginmedia.com/virginmedia.com.config.js --output=./sites/virginmedia.com/virginmedia.com.channels.xml +// npm run grab -- --site=virginmedia.com + +const { parser, url } = require('./virginmedia.com.config.js') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-03-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '1761', + xmltv_id: 'ViaplaySports1.uk' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/1' + ) +}) + +it('can parse response', done => { + const content = + '{"entryCount":410,"totalResults":410,"updated":1647459686755,"expires":1647460298218,"title":"EPG","periods":4,"periodStartTime":1647475200000,"periodEndTime":1647496800000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","t":"Live: NHL Hockey","s":1647473400000,"e":1647484200000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + + axios.get.mockImplementation(url => { + if ( + url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/2' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":410,"totalResults":410,"updated":1647460887411,"expires":1647461895572,"title":"EPG","periods":4,"periodStartTime":1647496800000,"periodEndTime":1647518400000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","t":"Challenge Cup Ice Hockey","s":1647484200000,"e":1647496800000,"c":"lgi-gb-prodobo-master:genre-123","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/3' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461910282,"title":"EPG","periods":4,"periodStartTime":1647518400000,"periodEndTime":1647540000000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","t":"NHL Hockey","s":1647511200000,"e":1647518400000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/programschedules/20220317/4' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"entryCount":410,"totalResults":410,"updated":1647460871713,"expires":1647461920720,"title":"EPG","periods":4,"periodStartTime":1647540000000,"periodEndTime":1647561600000,"entries":[{"o":"lgi-gb-prodobo-master:1761","l":[{"i":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","t":"Boxing World Weekly","s":1647539100000,"e":1647540900000,"c":"lgi-gb-prodobo-master:genre-27","a":false,"r":true,"rm":true,"rs":0,"re":2592000,"rst":"cloud","ra":false,"ad":[],"sl":[]}]}]}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","startTime":1647473400000,"endTime":1647484200000,"actualStartTime":1647473400000,"actualEndTime":1647484200000,"expirationDate":1648078200000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:de610af9a9b049c8a0245173f273136d36458f6f","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005,imi:de610af9a9b049c8a0245173f273136d36458f6f","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763419~~2FEP013520125005","title":"Live: NHL Hockey","description":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","longDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_h8_aa.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p21763419_tb2_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","shortDescription":"The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"20120000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Boston Bruins at Minnesota Wild"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH013520120000","rootId":"crid:~~2F~~2Fgn.tv~~2F8396306~~2FSH013520120000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647484200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763419|en-GB"}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","startTime":1647484200000,"endTime":1647496800000,"actualStartTime":1647484200000,"actualEndTime":1647496800000,"expirationDate":1648089000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005,imi:d4324f579ad36992f0c3f6e6d35a9b93e98cb78a","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21720572~~2FEP021779870005","title":"Challenge Cup Ice Hockey","description":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","longDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p11743980_b_v12_aa.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","shortDescription":"Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"2022031605","seriesNumber":"79870000","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"Final: Belfast Giants v Cardiff Devils"},"parentId":"crid:~~2F~~2Fgn.tv~~2F123456789~~2FSH021779870000","rootId":"crid:~~2F~~2Fgn.tv~~2F11743980~~2FSH021779870000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647928800000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21720572|en-GB"}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","startTime":1647511200000,"endTime":1647518400000,"actualStartTime":1647511200000,"actualEndTime":1647518400000,"expirationDate":1648116000000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435,imi:9692f5ceb0b63354262339e8529e3a9cb57add9c","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21763550~~2FEP012830215435","title":"NHL Hockey","description":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","longDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-123","title":"Ice Hockey","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResLandscapeProductionStill","assetTypes":["HighResLandscapeProductionStill"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_h8_ak.jpg"},{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p448880_b_v12_ak.jpg"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","shortDescription":"The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"194","seriesNumber":"102022","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[],"secondaryTitle":"New Jersey Devils at Calgary Flames"},"parentId":"crid:~~2F~~2Fgn.tv~~2F21275201~~2FSH012830210000","rootId":"crid:~~2F~~2Fgn.tv~~2F448880~~2FSH012830210000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1647583200000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21763550|en-GB"}' + ) + }) + } else if ( + url === + 'https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/listings/crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489' + ) { + return Promise.resolve({ + data: JSON.parse( + '{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","startTime":1647539100000,"endTime":1647540900000,"actualStartTime":1647539100000,"actualEndTime":1647540900000,"expirationDate":1648143900000,"stationId":"lgi-gb-prodobo-master:1761","imi":"imi:c02da14358110cec07d14dc154717ce62ba2f489","scCridImi":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145,imi:c02da14358110cec07d14dc154717ce62ba2f489","mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","program":{"id":"crid:~~2F~~2Fgn.tv~~2F21764379~~2FEP025886890145","title":"Boxing World Weekly","description":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","longDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","medium":"TV","categories":[{"id":"lgi-gb-prodobo-master:genre-27","title":"Sport","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"},{"id":"lgi-gb-prodobo-master:genre-83","title":"Boxing","scheme":"urn:libertyglobal:metadata:cs:ContentCS:2014_1"}],"isAdult":false,"cast":[],"directors":[],"images":[{"assetType":"HighResPortrait","assetTypes":["HighResPortrait"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p19340143_b_v8_aa.jpg"},{"assetType":"TitleTreatment","assetTypes":["TitleTreatment"],"url":"https://staticqbr-gb-prod.prod.cdn.dmdsdp.com/image-service/ImagesEPG/EventImages/p13641079_ttl_h95_aa.png"}],"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","parentalRatingDescription":[],"resolutions":[],"mediaGroupId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","shortDescription":"A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.","mediaType":"Episode","year":"2022","seriesEpisodeNumber":"60","seriesNumber":"4","videos":[],"videoStreams":[],"entitlements":["VIP","_OPEN_"],"currentProductIds":[],"currentTvodProductIds":[]},"parentId":"crid:~~2F~~2Fgn.tv~~2F19340143~~2FSH025886890000","rootId":"crid:~~2F~~2Fgn.tv~~2F13641079~~2FSH025886890000","replayTvAvailable":true,"audioTracks":[{"lang":"en","audioPurpose":"main"}],"ratings":[],"offersLatestExpirationDate":1648142400000,"replayTvStartOffset":0,"replayTvEndOffset":2592000,"replayEnabledOnMobileClients":true,"replaySource":"cloud","isGoReplayableViaExternalApp":false,"mergedId":"21764379|en-GB"}' + ) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + parser({ content, channel, date }) + .then(result => { + result = result.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-03-16T23:30:00.000Z', + stop: '2022-03-17T02:30:00.000Z', + title: 'Live: NHL Hockey', + description: + 'The Boston Bruins make the trip to Xcel Energy Center for an NHL clash with the Minnesota Wild.', + category: ['Sport', 'Ice Hockey'] + }, + { + start: '2022-03-17T02:30:00.000Z', + stop: '2022-03-17T06:00:00.000Z', + title: 'Challenge Cup Ice Hockey', + description: + 'Exclusive coverage from SSE Arena of the Premier Sports Challenge Final between Belfast Giants and Cardiff Devils.', + category: ['Ice Hockey'] + }, + { + start: '2022-03-17T10:00:00.000Z', + stop: '2022-03-17T12:00:00.000Z', + title: 'NHL Hockey', + description: + 'The Calgary Flames play host to the New Jersey Devils in this NHL encounter from Scotiabank Saddledome.', + category: ['Sport', 'Ice Hockey'] + }, + { + start: '2022-03-17T17:45:00.000Z', + stop: '2022-03-17T18:15:00.000Z', + title: 'Boxing World Weekly', + description: + 'A weekly series designed to showcase the best of our sport. Boxing World features news, highlights, previews and profiles from the world of pro boxing.', + category: ['Sport', 'Boxing'], + season: '4', + episode: '60' + } + ]) + done() + }) + .catch(done) +}) + +it('can handle empty guide', done => { + parser({ + content: '[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]', + channel, + date + }) + .then(result => { + expect(result).toMatchObject([]) + done() + }) + .catch(done) +}) diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js index b1fa6144..23c8c34c 100644 --- a/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js +++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js @@ -1,82 +1,82 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'virginmediatelevision.ie', - days: 2, - url({ date }) { - return `https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=${date.format( - 'YYYY-MM-DD' - )}` - }, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1h - } - }, - parser({ content, channel, date }) { - const programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - const $item = cheerio.load(item) - let start = parseStart($item, date) - let duration = parseDuration($item) - let stop = start.add(duration, 'm') - programs.push({ - title: parseTitle($item), - description: parseDescription($item), - sub_title: parseSubTitle($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.info > h2').text().trim() -} - -function parseDescription($item) { - return $item('.info').data('description') -} - -function parseSubTitle($item) { - return $item('.info').data('subtitle') -} - -function parseIcon($item) { - return $item('.info').data('image') -} - -function parseStart($item, date) { - const [time] = $item('.info') - .data('time') - .match(/^\d{1,2}\.\d{2}(am|pm)/) || [null] - - if (!time) return null - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h.mma', 'Europe/London') -} - -function parseDuration($item) { - const duration = $item('.info > .time').data('minutes') - - return duration ? parseInt(duration) : 30 -} - -function parseItems(content, channel) { - const $ = cheerio.load(content) - - return $(`.programs_parent > .programs[data-channel='${channel.site_id}'] > .program`).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'virginmediatelevision.ie', + days: 2, + url({ date }) { + return `https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=${date.format( + 'YYYY-MM-DD' + )}` + }, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1h + } + }, + parser({ content, channel, date }) { + const programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + const $item = cheerio.load(item) + let start = parseStart($item, date) + let duration = parseDuration($item) + let stop = start.add(duration, 'm') + programs.push({ + title: parseTitle($item), + description: parseDescription($item), + sub_title: parseSubTitle($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.info > h2').text().trim() +} + +function parseDescription($item) { + return $item('.info').data('description') +} + +function parseSubTitle($item) { + return $item('.info').data('subtitle') +} + +function parseIcon($item) { + return $item('.info').data('image') +} + +function parseStart($item, date) { + const [time] = $item('.info') + .data('time') + .match(/^\d{1,2}\.\d{2}(am|pm)/) || [null] + + if (!time) return null + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h.mma', 'Europe/London') +} + +function parseDuration($item) { + const duration = $item('.info > .time').data('minutes') + + return duration ? parseInt(duration) : 30 +} + +function parseItems(content, channel) { + const $ = cheerio.load(content) + + return $(`.programs_parent > .programs[data-channel='${channel.site_id}'] > .program`).toArray() +} diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js index 9f81f2ff..f18bd382 100644 --- a/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js +++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js @@ -1,53 +1,53 @@ -// npm run grab -- --site=virginmediatelevision.ie - -const { parser, url } = require('./virginmediatelevision.ie.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-31', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'one', - xmltv_id: 'VirginMediaOne.ie' -} - -it('can generate valid url', () => { - expect(url({ date })).toBe( - 'https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=2023-01-31' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(23) - expect(results[0]).toMatchObject({ - start: '2023-01-31T00:00:00.000Z', - stop: '2023-01-31T01:00:00.000Z', - title: 'Chasing Shadows', - sub_title: '', - description: - 'A detective sergeant and expert in the field of serial killers working for the Missing Persons Bureau tries to protect the general public from evil.', - icon: 'https://bcboltvirgin.akamaized.net/player/shows/1498_517x291_1528141264.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - channel, - content: '' - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=virginmediatelevision.ie + +const { parser, url } = require('./virginmediatelevision.ie.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-31', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'one', + xmltv_id: 'VirginMediaOne.ie' +} + +it('can generate valid url', () => { + expect(url({ date })).toBe( + 'https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=2023-01-31' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(23) + expect(results[0]).toMatchObject({ + start: '2023-01-31T00:00:00.000Z', + stop: '2023-01-31T01:00:00.000Z', + title: 'Chasing Shadows', + sub_title: '', + description: + 'A detective sergeant and expert in the field of serial killers working for the Missing Persons Bureau tries to protect the general public from evil.', + icon: 'https://bcboltvirgin.akamaized.net/player/shows/1498_517x291_1528141264.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + channel, + content: '' + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/visionplus.id/visionplus.id.config.js b/sites/visionplus.id/visionplus.id.config.js index c1a88030..ddc68d78 100644 --- a/sites/visionplus.id/visionplus.id.config.js +++ b/sites/visionplus.id/visionplus.id.config.js @@ -1,91 +1,91 @@ -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const cheerio = require('cheerio') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'visionplus.id', - days: 2, - request: { - headers: { - Authorization: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' - } - }, - url({ date, channel }) { - return `https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=${date.format( - 'YYYY-MM-DD' - )}&channel_ids=${channel.site_id}` - }, - parser({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel, date) - items.forEach(item => { - let prev = programs[programs.length - 1] - let start = parseStart(item, date) - let stop = parseStop(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - } - if (stop.isBefore(prev.stop)) { - stop = stop.add(1, 'd') - } - } - - programs.push({ - title: item.t, - description: item.synopsis, - start, - stop - }) - }) - - return programs - }, - async channels() { - const xml = await axios - .get('https://www.visionplus.id/sitemap-channels.xml') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(xml) - const items = $('url').toArray() - - return items.map(item => { - const $item = cheerio.load(item) - const loc = $item('loc').text() - const [, site_id] = loc.match(/channel\/(\d+)\//) || [null, null] - - return { - site_id, - name: $item('video\\:title').text().trim() - } - }) - } -} - -function parseStart(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.s}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseStop(item, date) { - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.e}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') -} - -function parseItems(content, channel, date) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.data)) return [] - const channelData = data.data.find(c => c.id === channel.site_id) - if (!channelData || !Array.isArray(channelData.schedules)) return [] - const daySchedule = channelData.schedules.find(d => d.date === date.format('YYYY-MM-DD')) - if (!daySchedule || !Array.isArray(daySchedule.items)) return [] - - return daySchedule.items -} +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const cheerio = require('cheerio') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'visionplus.id', + days: 2, + request: { + headers: { + Authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' + } + }, + url({ date, channel }) { + return `https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=${date.format( + 'YYYY-MM-DD' + )}&channel_ids=${channel.site_id}` + }, + parser({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel, date) + items.forEach(item => { + let prev = programs[programs.length - 1] + let start = parseStart(item, date) + let stop = parseStop(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + } + if (stop.isBefore(prev.stop)) { + stop = stop.add(1, 'd') + } + } + + programs.push({ + title: item.t, + description: item.synopsis, + start, + stop + }) + }) + + return programs + }, + async channels() { + const xml = await axios + .get('https://www.visionplus.id/sitemap-channels.xml') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(xml) + const items = $('url').toArray() + + return items.map(item => { + const $item = cheerio.load(item) + const loc = $item('loc').text() + const [, site_id] = loc.match(/channel\/(\d+)\//) || [null, null] + + return { + site_id, + name: $item('video\\:title').text().trim() + } + }) + } +} + +function parseStart(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.s}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseStop(item, date) { + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${item.e}`, 'YYYY-MM-DD HH:mm', 'Asia/Jakarta') +} + +function parseItems(content, channel, date) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.data)) return [] + const channelData = data.data.find(c => c.id === channel.site_id) + if (!channelData || !Array.isArray(channelData.schedules)) return [] + const daySchedule = channelData.schedules.find(d => d.date === date.format('YYYY-MM-DD')) + if (!daySchedule || !Array.isArray(daySchedule.items)) return [] + + return daySchedule.items +} diff --git a/sites/visionplus.id/visionplus.id.test.js b/sites/visionplus.id/visionplus.id.test.js index 430ecbf3..881db82b 100644 --- a/sites/visionplus.id/visionplus.id.test.js +++ b/sites/visionplus.id/visionplus.id.test.js @@ -1,64 +1,64 @@ -// npm run channels:parse -- --config=./sites/visionplus.id/visionplus.id.config.js --output=./sites/visionplus.id/visionplus.id.channels.xml -// npm run grab -- --site=visionplus.id - -const { parser, url, request } = require('./visionplus.id.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-06-30', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2', - xmltv_id: 'RCTI.id' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=2023-06-30&channel_ids=2' - ) -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - Authorization: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' - }) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - let results = parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(18) - expect(results[0]).toMatchObject({ - start: '2023-06-29T18:15:00.000Z', - stop: '2023-06-29T19:00:00.000Z', - title: 'Hafalan Shalat Delisa', - description: '' - }) - - expect(results[17]).toMatchObject({ - start: '2023-06-30T16:15:00.000Z', - stop: '2023-06-30T18:15:00.000Z', - title: 'Tukang Bubur Pulang Haji', - description: '' - }) -}) - -it('can handle empty guide', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) - const results = parser({ content, channel }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/visionplus.id/visionplus.id.config.js --output=./sites/visionplus.id/visionplus.id.channels.xml +// npm run grab -- --site=visionplus.id + +const { parser, url, request } = require('./visionplus.id.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-06-30', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2', + xmltv_id: 'RCTI.id' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://epg-api.visionplus.id/api/v1/epg?isLive=false&start_time_from=2023-06-30&channel_ids=2' + ) +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + Authorization: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5NDY0NTE4OTcsInVpZCI6MCwicGwiOiJ3ZWIiLCJndWVzdF90b2tlbiI6ImNhNGNjMjdiNzc3MjBjODEwNzQ2YzY3MTY4NzNjMDI3NGU4NWYxMWQifQ.tt08jLZ3HiNadUeSgc9O-nhIzEi7WMYRjxMb05lEZ74' + }) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + let results = parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(18) + expect(results[0]).toMatchObject({ + start: '2023-06-29T18:15:00.000Z', + stop: '2023-06-29T19:00:00.000Z', + title: 'Hafalan Shalat Delisa', + description: '' + }) + + expect(results[17]).toMatchObject({ + start: '2023-06-30T16:15:00.000Z', + stop: '2023-06-30T18:15:00.000Z', + title: 'Tukang Bubur Pulang Haji', + description: '' + }) +}) + +it('can handle empty guide', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json')) + const results = parser({ content, channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/vivacom.bg/vivacom.bg.config.js b/sites/vivacom.bg/vivacom.bg.config.js index eaf37a22..53ea4edf 100644 --- a/sites/vivacom.bg/vivacom.bg.config.js +++ b/sites/vivacom.bg/vivacom.bg.config.js @@ -1,77 +1,77 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'vivacom.bg', - days: 2, - url({ date, channel }) { - const [page] = channel.site_id.split('#') - - return `https://www.vivacom.bg/bg/tv/programa/?date=${date.format('YYYY-MM-DD')}&page=${page}` - }, - parser: function ({ content, channel, date }) { - let programs = [] - const items = parseItems(content, channel) - items.forEach(item => { - programs.push({ - title: parseTitle(item), - description: parseDescription(item), - start: parseStart(item, date), - stop: parseStop(item, date) - }) - }) - - return programs - } -} - -function parseStart(item, date) { - const $ = cheerio.load(item) - let [, time] = $('span') - .text() - .match(/^(\d{2}:\d{2}:\d{2})/) || [null, null] - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() -} - -function parseStop(item, date) { - const $ = cheerio.load(item) - let [, time] = $('span') - .text() - .match(/(\d{2}:\d{2}:\d{2})$/) || [null, null] - if (!time) return null - if (time === '00:00:00') date = date.add(1, 'd') - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() -} - -function parseTitle(item) { - const $ = cheerio.load(item) - - return $('h3').text() -} - -function parseDescription(item) { - const $ = cheerio.load(item) - - return $('p').text() -} - -function parseItems(content, channel) { - const [, channelId] = channel.site_id.split('#') - const $ = cheerio.load(content) - const listItem = $(`#scroll-vertical > li[title="${channelId}"]`) - const i = $('#scroll-vertical > li').index(listItem) - - return $(`#scroll-horizontal > ul:nth-child(${i + 1}) li`).toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'vivacom.bg', + days: 2, + url({ date, channel }) { + const [page] = channel.site_id.split('#') + + return `https://www.vivacom.bg/bg/tv/programa/?date=${date.format('YYYY-MM-DD')}&page=${page}` + }, + parser: function ({ content, channel, date }) { + let programs = [] + const items = parseItems(content, channel) + items.forEach(item => { + programs.push({ + title: parseTitle(item), + description: parseDescription(item), + start: parseStart(item, date), + stop: parseStop(item, date) + }) + }) + + return programs + } +} + +function parseStart(item, date) { + const $ = cheerio.load(item) + let [, time] = $('span') + .text() + .match(/^(\d{2}:\d{2}:\d{2})/) || [null, null] + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() +} + +function parseStop(item, date) { + const $ = cheerio.load(item) + let [, time] = $('span') + .text() + .match(/(\d{2}:\d{2}:\d{2})$/) || [null, null] + if (!time) return null + if (time === '00:00:00') date = date.add(1, 'd') + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD HH:mm:ss', 'Europe/Sofia').toJSON() +} + +function parseTitle(item) { + const $ = cheerio.load(item) + + return $('h3').text() +} + +function parseDescription(item) { + const $ = cheerio.load(item) + + return $('p').text() +} + +function parseItems(content, channel) { + const [, channelId] = channel.site_id.split('#') + const $ = cheerio.load(content) + const listItem = $(`#scroll-vertical > li[title="${channelId}"]`) + const i = $('#scroll-vertical > li').index(listItem) + + return $(`#scroll-horizontal > ul:nth-child(${i + 1}) li`).toArray() +} diff --git a/sites/vivacom.bg/vivacom.bg.test.js b/sites/vivacom.bg/vivacom.bg.test.js index c47f1810..44b9e1d9 100644 --- a/sites/vivacom.bg/vivacom.bg.test.js +++ b/sites/vivacom.bg/vivacom.bg.test.js @@ -1,46 +1,46 @@ -// npm run grab -- --site=vivacom.bg - -const { parser, url } = require('./vivacom.bg.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '1#БНТ 1 HD', xmltv_id: 'BNT1.bg' } -const content = - '
        • Когато сърцето зове

          04:25:00 - 05:10:00

          Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/

        • Dreamy Nights: Songs & Rhymes

          23:30:00 - 00:00:00

          Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight

        ' - -it('can generate valid url', () => { - const result = url({ date, channel }) - expect(result).toBe('https://www.vivacom.bg/bg/tv/programa/?date=2021-11-05&page=1') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-05T02:25:00.000Z', - stop: '2021-11-05T03:10:00.000Z', - title: 'Когато сърцето зове', - description: 'Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/' - }, - { - start: '2021-11-05T21:30:00.000Z', - stop: '2021-11-05T22:00:00.000Z', - title: 'Dreamy Nights: Songs & Rhymes', - description: 'Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: - '
        ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=vivacom.bg + +const { parser, url } = require('./vivacom.bg.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-05', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '1#БНТ 1 HD', xmltv_id: 'BNT1.bg' } +const content = + '
        • Когато сърцето зове

          04:25:00 - 05:10:00

          Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/

        • Dreamy Nights: Songs & Rhymes

          23:30:00 - 00:00:00

          Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight

        ' + +it('can generate valid url', () => { + const result = url({ date, channel }) + expect(result).toBe('https://www.vivacom.bg/bg/tv/programa/?date=2021-11-05&page=1') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-05T02:25:00.000Z', + stop: '2021-11-05T03:10:00.000Z', + title: 'Когато сърцето зове', + description: 'Телевизионен филм. Тв филм /4 сезон, 5 епизод/п/' + }, + { + start: '2021-11-05T21:30:00.000Z', + stop: '2021-11-05T22:00:00.000Z', + title: 'Dreamy Nights: Songs & Rhymes', + description: 'Songs & Rhymes, Flowers, Milky Way, Close Your Eyes, Twilight' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: + '
        ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/vtm.be/vtm.be.config.js b/sites/vtm.be/vtm.be.config.js index 7dbe8b21..21fddf2f 100644 --- a/sites/vtm.be/vtm.be.config.js +++ b/sites/vtm.be/vtm.be.config.js @@ -1,48 +1,48 @@ -const dayjs = require('dayjs') -const isBetween = require('dayjs/plugin/isBetween') - -dayjs.extend(isBetween) - -module.exports = { - site: 'vtm.be', - days: 2, - url: function ({ channel }) { - return `https://vtm.be/tv-gids/${channel.site_id}` - }, - request: { - headers: { - Cookie: - 'ak_bmsc=8103DDA2C2C37ECD922124463C746A4C~000000000000000000000000000000~YAAQNwVJF7ndI+p8AQAAYDkcCg0mQAkQ2jDHjSfnXl9VIGnzditECZ1FDj1Yi72a8rv/Q454lDDY0Dm3TPqxJUuNLzxJGmgkLmei4IIIwzKJWbB6wC/FMQApoI1NbGz+tUErryic1HWdbZ2dz1IX+AkOHJ9RVupYG5GmkSEQdFG1+/dSZoBMWEeb/5VOCLmNXRDP7k8LnSXaIuKqp5c2MQB+uQ9DdHUd6bIje3dzuxbka9+nJZ+eX/pNbgWI41X2tiXLvPZKh91Tk9k98zrK0pwBnGpTJqDVxmafYH/CjkXoLgEUW3loZfgL9SqddG706a4LnRPhyLzW6W6SH7Q0QOFE4g54NKADVttS2gbXgVrICvo0bb0FAESaFjc5uDyOd+fV2XBGzw==; authId=54da9bc2-d387-4923-8773-3d33ec68710e; gtm_session=1; _sp_ses.417f=*; _ga=GA1.2.525677035.1636552212; _gid=GA1.2.386833723.1636552212; tcf20_purposes=functional|analytics|targeted_advertising|non-personalised_ads|personalisation|marketing|social_media|advertising_1|advertising_2|advertising_3|advertising_4|advertising_7|advertising_9|advertising_10; _gcl_au=1.1.112810754.1636552212; _gat_UA-538372-57=1; sp=4a32f074-5526-4654-9389-2516d799ec68; _gat_UA-6602938-21=1; _sp_id.417f=0c81a857-09dc-47c2-8e51-4fed976211c4.1636552212.1.1636552214.1636552212.55934f90-4bad-47ff-8c5e-cf904126dcfb; bm_sv=1A45EF31D80D05B688C17EAD85964E29~hFpINNxpFphfJ2LLPoLQTauvUpyAf3kaTeGZAMfI/UTMlTRFjoAGBQJPEUPvSw3rXw/swqqAICc74l56pEBVSw6aJYqaoRaiRAZXyWZzQ6jAoeP5SMsZwtvNzYQ3aJXVWM8W8a98J0trlnSjIIsRPQ==' - } - }, - parser: function ({ content, date }) { - let programs = [] - const data = parseContent(content) - const items = parseItems(data, date) - items.forEach(item => { - programs.push({ - title: item.title, - description: item.synopsis, - category: item.genre, - icon: item.imageUrl, - start: dayjs(item.from).toJSON(), - stop: dayjs(item.to).toJSON() - }) - }) - - return programs - } -} - -function parseContent(content) { - const [, json] = content.match(/window.__EPG_REDUX_DATA__=(.*);\n/i) || [null, null] - const data = JSON.parse(json) - - return data -} - -function parseItems(data, date) { - if (!data || !data.broadcasts) return [] - - return Object.values(data.broadcasts).filter(i => dayjs(i.from).isBetween(date, date.add(1, 'd'))) -} +const dayjs = require('dayjs') +const isBetween = require('dayjs/plugin/isBetween') + +dayjs.extend(isBetween) + +module.exports = { + site: 'vtm.be', + days: 2, + url: function ({ channel }) { + return `https://vtm.be/tv-gids/${channel.site_id}` + }, + request: { + headers: { + Cookie: + 'ak_bmsc=8103DDA2C2C37ECD922124463C746A4C~000000000000000000000000000000~YAAQNwVJF7ndI+p8AQAAYDkcCg0mQAkQ2jDHjSfnXl9VIGnzditECZ1FDj1Yi72a8rv/Q454lDDY0Dm3TPqxJUuNLzxJGmgkLmei4IIIwzKJWbB6wC/FMQApoI1NbGz+tUErryic1HWdbZ2dz1IX+AkOHJ9RVupYG5GmkSEQdFG1+/dSZoBMWEeb/5VOCLmNXRDP7k8LnSXaIuKqp5c2MQB+uQ9DdHUd6bIje3dzuxbka9+nJZ+eX/pNbgWI41X2tiXLvPZKh91Tk9k98zrK0pwBnGpTJqDVxmafYH/CjkXoLgEUW3loZfgL9SqddG706a4LnRPhyLzW6W6SH7Q0QOFE4g54NKADVttS2gbXgVrICvo0bb0FAESaFjc5uDyOd+fV2XBGzw==; authId=54da9bc2-d387-4923-8773-3d33ec68710e; gtm_session=1; _sp_ses.417f=*; _ga=GA1.2.525677035.1636552212; _gid=GA1.2.386833723.1636552212; tcf20_purposes=functional|analytics|targeted_advertising|non-personalised_ads|personalisation|marketing|social_media|advertising_1|advertising_2|advertising_3|advertising_4|advertising_7|advertising_9|advertising_10; _gcl_au=1.1.112810754.1636552212; _gat_UA-538372-57=1; sp=4a32f074-5526-4654-9389-2516d799ec68; _gat_UA-6602938-21=1; _sp_id.417f=0c81a857-09dc-47c2-8e51-4fed976211c4.1636552212.1.1636552214.1636552212.55934f90-4bad-47ff-8c5e-cf904126dcfb; bm_sv=1A45EF31D80D05B688C17EAD85964E29~hFpINNxpFphfJ2LLPoLQTauvUpyAf3kaTeGZAMfI/UTMlTRFjoAGBQJPEUPvSw3rXw/swqqAICc74l56pEBVSw6aJYqaoRaiRAZXyWZzQ6jAoeP5SMsZwtvNzYQ3aJXVWM8W8a98J0trlnSjIIsRPQ==' + } + }, + parser: function ({ content, date }) { + let programs = [] + const data = parseContent(content) + const items = parseItems(data, date) + items.forEach(item => { + programs.push({ + title: item.title, + description: item.synopsis, + category: item.genre, + icon: item.imageUrl, + start: dayjs(item.from).toJSON(), + stop: dayjs(item.to).toJSON() + }) + }) + + return programs + } +} + +function parseContent(content) { + const [, json] = content.match(/window.__EPG_REDUX_DATA__=(.*);\n/i) || [null, null] + const data = JSON.parse(json) + + return data +} + +function parseItems(data, date) { + if (!data || !data.broadcasts) return [] + + return Object.values(data.broadcasts).filter(i => dayjs(i.from).isBetween(date, date.add(1, 'd'))) +} diff --git a/sites/vtm.be/vtm.be.test.js b/sites/vtm.be/vtm.be.test.js index 178f7f64..172eaea9 100644 --- a/sites/vtm.be/vtm.be.test.js +++ b/sites/vtm.be/vtm.be.test.js @@ -1,45 +1,45 @@ -// npm run grab -- --site=vtm.be - -const { parser, url } = require('./vtm.be.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'vtm', - xmltv_id: 'VTM.be' -} -const content = ` ` - -it('can generate valid url', () => { - const result = url({ channel }) - expect(result).toBe('https://vtm.be/tv-gids/vtm') -}) - -it('can parse response', () => { - const result = parser({ date, channel, content }) - expect(result).toMatchObject([ - { - start: '2021-11-10T23:45:00.000Z', - stop: '2021-11-11T00:20:00.000Z', - title: 'Wooninspiraties', - icon: 'https://images4.persgroep.net/rcs/z5qrZHumkjuN5rWzoaRJ_BTdL7A/diocontent/209688322/_fill/600/400?appId=da11c75db9b73ea0f41f0cd0da631c71', - description: - 'Een team gaat op pad om inspiratie op te doen over alles wat met wonen en leven te maken heeft; Ze trekken heel het land door om de laatste trends en tips op het gebied van wonen te achterhalen.', - category: 'Magazine' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: ' ' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=vtm.be + +const { parser, url } = require('./vtm.be.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-10', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'vtm', + xmltv_id: 'VTM.be' +} +const content = ` ` + +it('can generate valid url', () => { + const result = url({ channel }) + expect(result).toBe('https://vtm.be/tv-gids/vtm') +}) + +it('can parse response', () => { + const result = parser({ date, channel, content }) + expect(result).toMatchObject([ + { + start: '2021-11-10T23:45:00.000Z', + stop: '2021-11-11T00:20:00.000Z', + title: 'Wooninspiraties', + icon: 'https://images4.persgroep.net/rcs/z5qrZHumkjuN5rWzoaRJ_BTdL7A/diocontent/209688322/_fill/600/400?appId=da11c75db9b73ea0f41f0cd0da631c71', + description: + 'Een team gaat op pad om inspiratie op te doen over alles wat met wonen en leven te maken heeft; Ze trekken heel het land door om de laatste trends en tips op het gebied van wonen te achterhalen.', + category: 'Magazine' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: ' ' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/walesi.com.fj/walesi.com.fj.config.js b/sites/walesi.com.fj/walesi.com.fj.config.js index 4b715edc..156582ad 100644 --- a/sites/walesi.com.fj/walesi.com.fj.config.js +++ b/sites/walesi.com.fj/walesi.com.fj.config.js @@ -1,91 +1,91 @@ -const axios = require('axios') -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'walesi.com.fj', - days: 2, - url: 'https://www.walesi.com.fj/wp-admin/admin-ajax.php', - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data({ channel, date }) { - const params = new URLSearchParams() - params.append('chanel', channel.site_id) - params.append('date', date.unix()) - params.append('action', 'extvs_get_schedule_simple') - - return params - } - }, - parser({ content, date }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const start = parseStart($item, date) - const stop = start.add(30, 'm') - if (prev) prev.stop = start - programs.push({ - title: parseTitle($item), - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.walesi.com.fj/channel-guide/') - .then(r => r.data) - .catch(console.log) - - const $ = cheerio.load(data) - const channels = $( - 'div.ex-chanel-list > div.extvs-inline-chanel > ul > li.extvs-inline-select' - ).toArray() - return channels.map(item => { - const $item = cheerio.load(item) - const [, name] = $item('span') - .text() - .trim() - .match(/\d+\. (.*)/) || [null, null] - return { - lang: 'fj', - site_id: $item('*').data('value'), - name - } - }) - } -} - -function parseTitle($item) { - return $item('td.extvs-table1-programme > div > div > figure > h3').text() -} - -function parseStart($item, date) { - let time = $item('td.extvs-table1-time > span').text().trim() - if (!time) return null - time = `${date.format('YYYY-MM-DD')} ${time}` - - return dayjs.tz(time, 'YYYY-MM-DD H:mm a', 'Pacific/Fiji') -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data.html) return [] - const $ = cheerio.load(data.html) - - return $('table > tbody > tr').toArray() -} +const axios = require('axios') +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'walesi.com.fj', + days: 2, + url: 'https://www.walesi.com.fj/wp-admin/admin-ajax.php', + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data({ channel, date }) { + const params = new URLSearchParams() + params.append('chanel', channel.site_id) + params.append('date', date.unix()) + params.append('action', 'extvs_get_schedule_simple') + + return params + } + }, + parser({ content, date }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const start = parseStart($item, date) + const stop = start.add(30, 'm') + if (prev) prev.stop = start + programs.push({ + title: parseTitle($item), + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.walesi.com.fj/channel-guide/') + .then(r => r.data) + .catch(console.log) + + const $ = cheerio.load(data) + const channels = $( + 'div.ex-chanel-list > div.extvs-inline-chanel > ul > li.extvs-inline-select' + ).toArray() + return channels.map(item => { + const $item = cheerio.load(item) + const [, name] = $item('span') + .text() + .trim() + .match(/\d+\. (.*)/) || [null, null] + return { + lang: 'fj', + site_id: $item('*').data('value'), + name + } + }) + } +} + +function parseTitle($item) { + return $item('td.extvs-table1-programme > div > div > figure > h3').text() +} + +function parseStart($item, date) { + let time = $item('td.extvs-table1-time > span').text().trim() + if (!time) return null + time = `${date.format('YYYY-MM-DD')} ${time}` + + return dayjs.tz(time, 'YYYY-MM-DD H:mm a', 'Pacific/Fiji') +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data.html) return [] + const $ = cheerio.load(data.html) + + return $('table > tbody > tr').toArray() +} diff --git a/sites/walesi.com.fj/walesi.com.fj.test.js b/sites/walesi.com.fj/walesi.com.fj.test.js index 05d834eb..30068848 100644 --- a/sites/walesi.com.fj/walesi.com.fj.test.js +++ b/sites/walesi.com.fj/walesi.com.fj.test.js @@ -1,68 +1,68 @@ -// npm run channels:parse -- --config=./sites/walesi.com.fj/walesi.com.fj.config.js --output=./sites/walesi.com.fj/walesi.com.fj.channels.xml -// npm run grab -- --site=walesi.com.fj - -const { parser, url, request } = require('./walesi.com.fj.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'fbc-2', - xmltv_id: 'FBCTV.fj' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.walesi.com.fj/wp-admin/admin-ajax.php') -}) - -it('can generate valid request method', () => { - expect(request.method).toBe('POST') -}) - -it('can generate valid request headers', () => { - expect(request.headers).toMatchObject({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }) -}) - -it('can generate valid request data', () => { - const result = request.data({ date, channel }) - expect(result.has('chanel')).toBe(true) - expect(result.has('date')).toBe(true) - expect(result.has('action')).toBe(true) -}) - -it('can parse response', () => { - const content = - '{"html":"\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\r\\n\\t\\r\\n\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n
        ImageTimeProgramme
        \\r\\n\\t\\t\\t12:00 am\\r\\n\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t

        Aljazeera

        \\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t
        6:00 am

        Move Fiji

        \\r\\n\\t\\t\\t\\t"}' - const result = parser({ content, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-20T12:00:00.000Z', - stop: '2021-11-20T18:00:00.000Z', - title: 'Aljazeera' - }, - { - start: '2021-11-20T18:00:00.000Z', - stop: '2021-11-20T18:30:00.000Z', - title: 'Move Fiji' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '{"html":"

        No matching records found

        "}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/walesi.com.fj/walesi.com.fj.config.js --output=./sites/walesi.com.fj/walesi.com.fj.channels.xml +// npm run grab -- --site=walesi.com.fj + +const { parser, url, request } = require('./walesi.com.fj.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-21', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'fbc-2', + xmltv_id: 'FBCTV.fj' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.walesi.com.fj/wp-admin/admin-ajax.php') +}) + +it('can generate valid request method', () => { + expect(request.method).toBe('POST') +}) + +it('can generate valid request headers', () => { + expect(request.headers).toMatchObject({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) +}) + +it('can generate valid request data', () => { + const result = request.data({ date, channel }) + expect(result.has('chanel')).toBe(true) + expect(result.has('date')).toBe(true) + expect(result.has('action')).toBe(true) +}) + +it('can parse response', () => { + const content = + '{"html":"\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\r\\n\\t\\r\\n\\t\\r\\n\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n
        ImageTimeProgramme
        \\r\\n\\t\\t\\t12:00 am\\r\\n\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t

        Aljazeera

        \\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t
        \\r\\n\\t\\t\\t
        \\r\\n\\t\\t\\t\\t\\t
        \\r\\n\\t
        6:00 am

        Move Fiji

        \\r\\n\\t\\t\\t\\t"}' + const result = parser({ content, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-20T12:00:00.000Z', + stop: '2021-11-20T18:00:00.000Z', + title: 'Aljazeera' + }, + { + start: '2021-11-20T18:00:00.000Z', + stop: '2021-11-20T18:30:00.000Z', + title: 'Move Fiji' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '{"html":"

        No matching records found

        "}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/watchyour.tv/watchyour.tv.config.js b/sites/watchyour.tv/watchyour.tv.config.js index 316ccfb3..19e4d64b 100644 --- a/sites/watchyour.tv/watchyour.tv.config.js +++ b/sites/watchyour.tv/watchyour.tv.config.js @@ -1,55 +1,55 @@ -const dayjs = require('dayjs') -const axios = require('axios') - -module.exports = { - site: 'watchyour.tv', - days: 2, - url: 'https://www.watchyour.tv/guide.json', - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - parser: function ({ content, date, channel }) { - let programs = [] - const items = parseItems(content, date, channel) - items.forEach(item => { - const start = parseStart(item) - const stop = start.add(parseInt(item.duration), 'm') - programs.push({ - title: item.name, - icon: item.icon, - category: item.category, - start, - stop - }) - }) - - return programs - }, - async channels() { - const data = await axios - .get('https://www.watchyour.tv/guide.json') - .then(r => r.data) - .catch(console.log) - - return data.map(item => ({ - site_id: item.id, - name: item.name - })) - } -} - -function parseStart(item) { - return dayjs.unix(parseInt(item.tms)) -} - -function parseItems(content, date, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!Array.isArray(data)) return [] - const channelData = data.find(i => i.id == channel.site_id) - if (!channelData || !Array.isArray(channelData.shows)) return [] - - return channelData.shows.filter(i => i.start_day === date.format('YYYY-MM-DD')) -} +const dayjs = require('dayjs') +const axios = require('axios') + +module.exports = { + site: 'watchyour.tv', + days: 2, + url: 'https://www.watchyour.tv/guide.json', + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + parser: function ({ content, date, channel }) { + let programs = [] + const items = parseItems(content, date, channel) + items.forEach(item => { + const start = parseStart(item) + const stop = start.add(parseInt(item.duration), 'm') + programs.push({ + title: item.name, + icon: item.icon, + category: item.category, + start, + stop + }) + }) + + return programs + }, + async channels() { + const data = await axios + .get('https://www.watchyour.tv/guide.json') + .then(r => r.data) + .catch(console.log) + + return data.map(item => ({ + site_id: item.id, + name: item.name + })) + } +} + +function parseStart(item) { + return dayjs.unix(parseInt(item.tms)) +} + +function parseItems(content, date, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!Array.isArray(data)) return [] + const channelData = data.find(i => i.id == channel.site_id) + if (!channelData || !Array.isArray(channelData.shows)) return [] + + return channelData.shows.filter(i => i.start_day === date.format('YYYY-MM-DD')) +} diff --git a/sites/watchyour.tv/watchyour.tv.test.js b/sites/watchyour.tv/watchyour.tv.test.js index c920892c..a5e1a170 100644 --- a/sites/watchyour.tv/watchyour.tv.test.js +++ b/sites/watchyour.tv/watchyour.tv.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/watchyour.tv/watchyour.tv.config.js --output=./sites/watchyour.tv/watchyour.tv.channels.xml -// npm run grab -- --site=watchyour.tv - -const { parser, url } = require('./watchyour.tv.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '735', - xmltv_id: 'TVSClassicSports.us' -} - -it('can generate valid url', () => { - expect(url).toBe('https://www.watchyour.tv/guide.json') -}) - -it('can parse response', () => { - const content = - '[{"name":"TVS Classic Sports","icon":"https://www.watchyour.tv/epg/channellogos/tvs-classic-sports.png","language":"English","id":"735","shows":[{"name":"1979 WVU vs Penn State","category":"Sports","start_day":"2022-10-03","start":"04:00:00","end_day":"2022-10-03","end":"06:00:45","duration":"121","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index-1664769600-7245.m3u8?token=f7410a9414f61579dced17ac1bbdb971","icon":"https://example.com/image.png","timezone":"+0000","tms":"1664769600"},{"name":"1958 NCAA University of Kentucky vs Seattle U","category":"Sports","start_day":"2022-10-04","start":"00:58:50","end_day":"2022-10-04","end":"01:44:11","duration":"46","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index.m3u8?token=93e7b201f544c87296076b73f9d880ae","icon":"","timezone":"+0000","tms":"1664845130"}]}]' - const result = parser({ content, date, channel }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-10-03T04:00:00.000Z', - stop: '2022-10-03T06:01:00.000Z', - title: '1979 WVU vs Penn State', - icon: 'https://example.com/image.png', - category: 'Sports' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '', - date, - channel - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/watchyour.tv/watchyour.tv.config.js --output=./sites/watchyour.tv/watchyour.tv.channels.xml +// npm run grab -- --site=watchyour.tv + +const { parser, url } = require('./watchyour.tv.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-10-03', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '735', + xmltv_id: 'TVSClassicSports.us' +} + +it('can generate valid url', () => { + expect(url).toBe('https://www.watchyour.tv/guide.json') +}) + +it('can parse response', () => { + const content = + '[{"name":"TVS Classic Sports","icon":"https://www.watchyour.tv/epg/channellogos/tvs-classic-sports.png","language":"English","id":"735","shows":[{"name":"1979 WVU vs Penn State","category":"Sports","start_day":"2022-10-03","start":"04:00:00","end_day":"2022-10-03","end":"06:00:45","duration":"121","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index-1664769600-7245.m3u8?token=f7410a9414f61579dced17ac1bbdb971","icon":"https://example.com/image.png","timezone":"+0000","tms":"1664769600"},{"name":"1958 NCAA University of Kentucky vs Seattle U","category":"Sports","start_day":"2022-10-04","start":"00:58:50","end_day":"2022-10-04","end":"01:44:11","duration":"46","url":"http://rpn1.bozztv.com/36bay2/gusa-tvs/index.m3u8?token=93e7b201f544c87296076b73f9d880ae","icon":"","timezone":"+0000","tms":"1664845130"}]}]' + const result = parser({ content, date, channel }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-10-03T04:00:00.000Z', + stop: '2022-10-03T06:01:00.000Z', + title: '1979 WVU vs Penn State', + icon: 'https://example.com/image.png', + category: 'Sports' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '', + date, + channel + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/wavve.com/wavve.com.config.js b/sites/wavve.com/wavve.com.config.js index 347852ed..59755129 100644 --- a/sites/wavve.com/wavve.com.config.js +++ b/sites/wavve.com/wavve.com.config.js @@ -1,62 +1,62 @@ -const axios = require('axios') -const { DateTime } = require('luxon') - -module.exports = { - site: 'wavve.com', - days: 2, - url: function ({ channel, date }) { - return `https://apis.pooq.co.kr/live/epgs/channels/${ - channel.site_id - }?startdatetime=${date.format('YYYY-MM-DD')}%2000%3A00&enddatetime=${date - .add(1, 'd') - .format('YYYY-MM-DD')}%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500` - }, - parser: function ({ content }) { - let programs = [] - const items = parseItems(content) - items.forEach(item => { - programs.push({ - title: item.title, - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = [] - - const data = await axios - .get( - 'https://apis.pooq.co.kr/live/epgs?enddatetime=2022-04-17%2019%3A00&genre=all&limit=500&startdatetime=2022-04-17%2016%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9' - ) - .then(r => r.data) - .catch(console.log) - - data.list.forEach(i => { - channels.push({ - name: i.channelname, - site_id: i.channelid, - lang: 'ko' - }) - }) - - return channels - } -} - -function parseStart(item) { - return DateTime.fromFormat(item.starttime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() -} - -function parseStop(item) { - return DateTime.fromFormat(item.endtime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() -} - -function parseItems(content) { - const data = JSON.parse(content) - if (!data || !Array.isArray(data.list)) return [] - - return data.list -} +const axios = require('axios') +const { DateTime } = require('luxon') + +module.exports = { + site: 'wavve.com', + days: 2, + url: function ({ channel, date }) { + return `https://apis.pooq.co.kr/live/epgs/channels/${ + channel.site_id + }?startdatetime=${date.format('YYYY-MM-DD')}%2000%3A00&enddatetime=${date + .add(1, 'd') + .format('YYYY-MM-DD')}%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500` + }, + parser: function ({ content }) { + let programs = [] + const items = parseItems(content) + items.forEach(item => { + programs.push({ + title: item.title, + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = [] + + const data = await axios + .get( + 'https://apis.pooq.co.kr/live/epgs?enddatetime=2022-04-17%2019%3A00&genre=all&limit=500&startdatetime=2022-04-17%2016%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9' + ) + .then(r => r.data) + .catch(console.log) + + data.list.forEach(i => { + channels.push({ + name: i.channelname, + site_id: i.channelid, + lang: 'ko' + }) + }) + + return channels + } +} + +function parseStart(item) { + return DateTime.fromFormat(item.starttime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() +} + +function parseStop(item) { + return DateTime.fromFormat(item.endtime, 'yyyy-MM-dd HH:mm', { zone: 'Asia/Seoul' }).toUTC() +} + +function parseItems(content) { + const data = JSON.parse(content) + if (!data || !Array.isArray(data.list)) return [] + + return data.list +} diff --git a/sites/wavve.com/wavve.com.test.js b/sites/wavve.com/wavve.com.test.js index 7939ef40..23959642 100644 --- a/sites/wavve.com/wavve.com.test.js +++ b/sites/wavve.com/wavve.com.test.js @@ -1,46 +1,46 @@ -// npm run channels:parse -- --config=sites/wavve.com/wavve.com.config.js --output=sites/wavve.com/wavve.com.channels.xml -// npm run grab -- --site=wavve.com - -const { parser, url } = require('./wavve.com.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2022-04-17', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'K01', - xmltv_id: 'KBS1TV.kr' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe( - 'https://apis.pooq.co.kr/live/epgs/channels/K01?startdatetime=2022-04-17%2000%3A00&enddatetime=2022-04-18%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500' - ) -}) - -it('can parse response', () => { - const content = - '{"pagecount":"37","count":"37","list":[{"cpid":"C3","channelid":"K01","channelname":"KBS 1TV","channelimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS-1TV-1.jpg","scheduleid":"K01_20220416223000","programid":"","title":"특파원 보고 세계는 지금","image":"wchimg.wavve.com/live/thumbnail/K01.jpg","starttime":"2022-04-16 22:30","endtime":"2022-04-16 23:15","timemachine":"Y","license":"y","livemarks":[],"targetage":"0","tvimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS 1TV-2.png","ispreorder":"n","preorderlink":"n","alarm":"n"}]}' - const result = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2022-04-16T13:30:00.000Z', - stop: '2022-04-16T14:15:00.000Z', - title: '특파원 보고 세계는 지금' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - content: '{"pagecount":"0","count":"0","list":[]}' - }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=sites/wavve.com/wavve.com.config.js --output=sites/wavve.com/wavve.com.channels.xml +// npm run grab -- --site=wavve.com + +const { parser, url } = require('./wavve.com.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2022-04-17', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'K01', + xmltv_id: 'KBS1TV.kr' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://apis.pooq.co.kr/live/epgs/channels/K01?startdatetime=2022-04-17%2000%3A00&enddatetime=2022-04-18%2000%3A00&apikey=E5F3E0D30947AA5440556471321BB6D9&limit=500' + ) +}) + +it('can parse response', () => { + const content = + '{"pagecount":"37","count":"37","list":[{"cpid":"C3","channelid":"K01","channelname":"KBS 1TV","channelimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS-1TV-1.jpg","scheduleid":"K01_20220416223000","programid":"","title":"특파원 보고 세계는 지금","image":"wchimg.wavve.com/live/thumbnail/K01.jpg","starttime":"2022-04-16 22:30","endtime":"2022-04-16 23:15","timemachine":"Y","license":"y","livemarks":[],"targetage":"0","tvimage":"img.pooq.co.kr/BMS/Channelimage30/image/KBS 1TV-2.png","ispreorder":"n","preorderlink":"n","alarm":"n"}]}' + const result = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2022-04-16T13:30:00.000Z', + stop: '2022-04-16T14:15:00.000Z', + title: '특파원 보고 세계는 지금' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + content: '{"pagecount":"0","count":"0","list":[]}' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js b/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js index 7a89a815..a1b99fee 100644 --- a/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js +++ b/sites/worldfishingnetwork.com/worldfishingnetwork.com.config.js @@ -1,79 +1,79 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'worldfishingnetwork.com', - days: 2, - url({ date }) { - return `https://www.worldfishingnetwork.com/schedule/77420?day=${date.format('ddd')}` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content) - items.forEach(item => { - let $item = cheerio.load(item) - const prev = programs[programs.length - 1] - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - sub_title: parseSubTitle($item), - description: parseDescription($item), - icon: parseIcon($item), - start, - stop - }) - }) - - return programs - } -} - -function parseTitle($item) { - return $item('.show-title > h3').text().trim() -} - -function parseSubTitle($item) { - return $item('.show-title').clone().children().remove().end().text().trim() -} - -function parseDescription($item) { - return $item('.show-title > p').text().trim() -} - -function parseIcon($item) { - const url = $item('.show-img > img').attr('src') - - return url ? `https:${url}` : null -} - -function parseStart($item, date) { - const time = $item('.show-time > h2').clone().children().remove().end().text().trim() - const period = $item('.show-time > h2 > span > strong').text().trim() - - return dayjs.tz( - `${date.format('YYYY-MM-DD')} ${time} ${period}`, - 'YYYY-MM-DD HH:mm A', - 'America/New_York' - ) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('.show-item').toArray() -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'worldfishingnetwork.com', + days: 2, + url({ date }) { + return `https://www.worldfishingnetwork.com/schedule/77420?day=${date.format('ddd')}` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content) + items.forEach(item => { + let $item = cheerio.load(item) + const prev = programs[programs.length - 1] + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + sub_title: parseSubTitle($item), + description: parseDescription($item), + icon: parseIcon($item), + start, + stop + }) + }) + + return programs + } +} + +function parseTitle($item) { + return $item('.show-title > h3').text().trim() +} + +function parseSubTitle($item) { + return $item('.show-title').clone().children().remove().end().text().trim() +} + +function parseDescription($item) { + return $item('.show-title > p').text().trim() +} + +function parseIcon($item) { + const url = $item('.show-img > img').attr('src') + + return url ? `https:${url}` : null +} + +function parseStart($item, date) { + const time = $item('.show-time > h2').clone().children().remove().end().text().trim() + const period = $item('.show-time > h2 > span > strong').text().trim() + + return dayjs.tz( + `${date.format('YYYY-MM-DD')} ${time} ${period}`, + 'YYYY-MM-DD HH:mm A', + 'America/New_York' + ) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('.show-item').toArray() +} diff --git a/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js b/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js index 0b5b609e..7677c7ca 100644 --- a/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js +++ b/sites/worldfishingnetwork.com/worldfishingnetwork.com.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=worldfishingnetwork.com - -const { parser, url } = require('./worldfishingnetwork.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-01-24', 'YYYY-MM-DD').startOf('d') - -it('can generate valid url', () => { - expect(url({ date })).toBe('https://www.worldfishingnetwork.com/schedule/77420?day=Tue') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') - let results = parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-24T05:00:00.000Z', - stop: '2023-01-24T07:00:00.000Z', - title: 'Major League Fishing', - sub_title: 'Challenge Cup Sudden Death Round 2', - description: - 'Nine anglers race to a target weight on Lake Wylie in the Lucas Oil Challenge Cup, presented by B&W Trailer Hitches, Rock Hill, South Carolina. Only four will move on to the Championship Round.', - icon: 'https://content.osgnetworks.tv/shows/major-league-fishing-thumbnail.jpg' - }) - - expect(results[41]).toMatchObject({ - start: '2023-01-25T04:30:00.000Z', - stop: '2023-01-25T05:00:00.000Z', - title: 'Fishing 411', - sub_title: 'Flint Wilderness Walleye', - description: - 'Mark Romanack and Bryan Darland fish walleye on Klotz Lake in the famed Flint Wilderness of Ontario', - icon: 'https://content.osgnetworks.tv/shows/fishin-411-thumbnail.jpg' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') - }) - - expect(results).toMatchObject([]) -}) +// npm run grab -- --site=worldfishingnetwork.com + +const { parser, url } = require('./worldfishingnetwork.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-01-24', 'YYYY-MM-DD').startOf('d') + +it('can generate valid url', () => { + expect(url({ date })).toBe('https://www.worldfishingnetwork.com/schedule/77420?day=Tue') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8') + let results = parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-24T05:00:00.000Z', + stop: '2023-01-24T07:00:00.000Z', + title: 'Major League Fishing', + sub_title: 'Challenge Cup Sudden Death Round 2', + description: + 'Nine anglers race to a target weight on Lake Wylie in the Lucas Oil Challenge Cup, presented by B&W Trailer Hitches, Rock Hill, South Carolina. Only four will move on to the Championship Round.', + icon: 'https://content.osgnetworks.tv/shows/major-league-fishing-thumbnail.jpg' + }) + + expect(results[41]).toMatchObject({ + start: '2023-01-25T04:30:00.000Z', + stop: '2023-01-25T05:00:00.000Z', + title: 'Fishing 411', + sub_title: 'Flint Wilderness Walleye', + description: + 'Mark Romanack and Bryan Darland fish walleye on Klotz Lake in the famed Flint Wilderness of Ontario', + icon: 'https://content.osgnetworks.tv/shows/fishin-411-thumbnail.jpg' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8') + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/xumo.tv/xumo.tv.config.js b/sites/xumo.tv/xumo.tv.config.js index 345be720..970e1652 100644 --- a/sites/xumo.tv/xumo.tv.config.js +++ b/sites/xumo.tv/xumo.tv.config.js @@ -1,133 +1,133 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' - -const client = axios.create({ - baseURL: API_ENDPOINT, - responseType: 'arraybuffer' -}) - -module.exports = { - site: 'xumo.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - const [offset] = channel.site_id.split('#') - - return `${API_ENDPOINT}/epg/10006/${date.format( - 'YYYYMMDD' - )}/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const d = date.format('YYYYMMDD') - const [offset] = channel.site_id.split('#') - const promises = [ - client.get( - `/epg/10006/${d}/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ), - client.get( - `/epg/10006/${d}/2.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ), - client.get( - `/epg/10006/${d}/3.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` - ) - ] - const results = await Promise.allSettled(promises) - results.forEach(r => { - if (r.status === 'fulfilled') { - items = items.concat(parseItems(r.value.data, channel)) - } - }) - - items.forEach(item => { - programs.push({ - title: item.title, - sub_title: item.episodeTitle, - description: parseDescription(item), - start: parseStart(item), - stop: parseStop(item) - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get( - 'https://valencia-app-mds.xumo.com/v2/channels/list/10006.json?sort=hybrid&geoId=unknown' - ) - .then(r => r.data.channel.item) - .catch(console.log) - - const promises = [ - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=0`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=50`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=100`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=150`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=200`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=250`), - axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=300`) - ] - - const output = [] - const results = await Promise.allSettled(promises) - results.forEach((r, i) => { - if (r.status !== 'fulfilled') return - - r.value.data.channels.forEach(item => { - const info = channels.find(c => c.guid.value == item.channelId) - - if (!info) { - console.log(item.channelId) - } - - output.push({ - site_id: `${i * 50}#${item.channelId}`, - name: info.title - }) - }) - }) - - return output - } -} - -function parseDescription(item) { - if (!item.descriptions) return null - - return item.descriptions.medium || item.descriptions.small || item.descriptions.tiny -} - -function parseStart(item) { - return dayjs(item.start) -} - -function parseStop(item) { - return dayjs(item.end) -} - -function parseItems(content, channel) { - if (!content) return [] - const [, channelId] = channel.site_id.split('#') - const data = JSON.parse(content) - if (!data || !Array.isArray(data.channels)) return [] - const channelData = data.channels.find(c => c.channelId == channelId) - if (!channelData || !Array.isArray(channelData.schedule)) return [] - - return channelData.schedule - .map(item => { - const details = data.assets[item.assetId] - if (!details) return null - - return { ...item, ...details } - }) - .filter(Boolean) -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' + +const client = axios.create({ + baseURL: API_ENDPOINT, + responseType: 'arraybuffer' +}) + +module.exports = { + site: 'xumo.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + const [offset] = channel.site_id.split('#') + + return `${API_ENDPOINT}/epg/10006/${date.format( + 'YYYYMMDD' + )}/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const d = date.format('YYYYMMDD') + const [offset] = channel.site_id.split('#') + const promises = [ + client.get( + `/epg/10006/${d}/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ), + client.get( + `/epg/10006/${d}/2.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ), + client.get( + `/epg/10006/${d}/3.json?f=asset.title&f=asset.descriptions&limit=50&offset=${offset}` + ) + ] + const results = await Promise.allSettled(promises) + results.forEach(r => { + if (r.status === 'fulfilled') { + items = items.concat(parseItems(r.value.data, channel)) + } + }) + + items.forEach(item => { + programs.push({ + title: item.title, + sub_title: item.episodeTitle, + description: parseDescription(item), + start: parseStart(item), + stop: parseStop(item) + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get( + 'https://valencia-app-mds.xumo.com/v2/channels/list/10006.json?sort=hybrid&geoId=unknown' + ) + .then(r => r.data.channel.item) + .catch(console.log) + + const promises = [ + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=0`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=50`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=100`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=150`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=200`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=250`), + axios.get(`${API_ENDPOINT}/epg/10006/19700101/0.json?limit=50&offset=300`) + ] + + const output = [] + const results = await Promise.allSettled(promises) + results.forEach((r, i) => { + if (r.status !== 'fulfilled') return + + r.value.data.channels.forEach(item => { + const info = channels.find(c => c.guid.value == item.channelId) + + if (!info) { + console.log(item.channelId) + } + + output.push({ + site_id: `${i * 50}#${item.channelId}`, + name: info.title + }) + }) + }) + + return output + } +} + +function parseDescription(item) { + if (!item.descriptions) return null + + return item.descriptions.medium || item.descriptions.small || item.descriptions.tiny +} + +function parseStart(item) { + return dayjs(item.start) +} + +function parseStop(item) { + return dayjs(item.end) +} + +function parseItems(content, channel) { + if (!content) return [] + const [, channelId] = channel.site_id.split('#') + const data = JSON.parse(content) + if (!data || !Array.isArray(data.channels)) return [] + const channelData = data.channels.find(c => c.channelId == channelId) + if (!channelData || !Array.isArray(channelData.schedule)) return [] + + return channelData.schedule + .map(item => { + const details = data.assets[item.assetId] + if (!details) return null + + return { ...item, ...details } + }) + .filter(Boolean) +} diff --git a/sites/xumo.tv/xumo.tv.test.js b/sites/xumo.tv/xumo.tv.test.js index a0556e8a..5af00a13 100644 --- a/sites/xumo.tv/xumo.tv.test.js +++ b/sites/xumo.tv/xumo.tv.test.js @@ -1,77 +1,77 @@ -// npm run channels:parse -- --config=./sites/xumo.tv/xumo.tv.config.js --output=./sites/xumo.tv/xumo.tv.channels.xml -// npm run grab -- --site=xumo.tv - -const { parser, url } = require('./xumo.tv.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios', () => { - return { - create: jest.fn().mockReturnValue({ - get: jest.fn() - }) - } -}) - -const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' - -const date = dayjs.utc('2022-11-06', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '0#99991247', - xmltv_id: 'NBCNewsNow.us' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - `${API_ENDPOINT}/epg/10006/20221106/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) - - axios.create().get.mockImplementation(url => { - if ( - url === - `${API_ENDPOINT}/epg/10006/20221106/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` - ) { - return Promise.resolve({ - data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-11-05T23:00:00.000Z', - stop: '2022-11-06T01:00:00.000Z', - title: 'Dateline', - sub_title: 'The Disappearance of Laci Peterson', - description: - "After following Laci Peterson's case for more than 15 years, the show delivers a comprehensive report with rarely seen interrogation video, new insight from prosecutors, and surprising details from Amber Frey, who helped uncover the truth." - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - content: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))), - channel, - date - }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/xumo.tv/xumo.tv.config.js --output=./sites/xumo.tv/xumo.tv.channels.xml +// npm run grab -- --site=xumo.tv + +const { parser, url } = require('./xumo.tv.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios', () => { + return { + create: jest.fn().mockReturnValue({ + get: jest.fn() + }) + } +}) + +const API_ENDPOINT = 'https://valencia-app-mds.xumo.com/v2' + +const date = dayjs.utc('2022-11-06', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '0#99991247', + xmltv_id: 'NBCNewsNow.us' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + `${API_ENDPOINT}/epg/10006/20221106/0.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0.json')) + + axios.create().get.mockImplementation(url => { + if ( + url === + `${API_ENDPOINT}/epg/10006/20221106/1.json?f=asset.title&f=asset.descriptions&limit=50&offset=0` + ) { + return Promise.resolve({ + data: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/content_1.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-11-05T23:00:00.000Z', + stop: '2022-11-06T01:00:00.000Z', + title: 'Dateline', + sub_title: 'The Disappearance of Laci Peterson', + description: + "After following Laci Peterson's case for more than 15 years, the show delivers a comprehensive report with rarely seen interrogation video, new insight from prosecutors, and surprising details from Amber Frey, who helped uncover the truth." + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + content: Buffer.from(fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))), + channel, + date + }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/zap.co.ao/zap.co.ao.config.js b/sites/zap.co.ao/zap.co.ao.config.js index 165900a0..267ac859 100644 --- a/sites/zap.co.ao/zap.co.ao.config.js +++ b/sites/zap.co.ao/zap.co.ao.config.js @@ -1,48 +1,48 @@ -const { DateTime } = require('luxon') -const axios = require('axios') - -module.exports = { - site: 'zap.co.ao', - days: 2, - url: function ({ date, channel }) { - return `https://zapon.zapsi.net/ao/m/api/epg/events?date=${date.format('YYYYMMDD')}&channel=${ - channel.site_id - }` - }, - parser: function ({ content }) { - const programs = [] - const items = parseItems(content) - if (!items.length) return programs - items.forEach(item => { - programs.push({ - title: item.programName, - description: item.programDescription, - category: item.categoryName, - start: DateTime.fromSeconds(item.utcBeginDate).toUTC(), - stop: DateTime.fromSeconds(item.utcEndDate).toUTC() - }) - }) - - return programs - }, - async channels() { - const channels = await axios - .get('https://zapon.zapsi.net/ao/m/api/epg/channels') - .then(r => r.data.data) - .catch(console.log) - - return channels.map(item => { - return { - lang: 'pt', - site_id: item.id, - name: item.name - } - }) - } -} - -function parseItems(content) { - const data = JSON.parse(content) - - return data.data || [] -} +const { DateTime } = require('luxon') +const axios = require('axios') + +module.exports = { + site: 'zap.co.ao', + days: 2, + url: function ({ date, channel }) { + return `https://zapon.zapsi.net/ao/m/api/epg/events?date=${date.format('YYYYMMDD')}&channel=${ + channel.site_id + }` + }, + parser: function ({ content }) { + const programs = [] + const items = parseItems(content) + if (!items.length) return programs + items.forEach(item => { + programs.push({ + title: item.programName, + description: item.programDescription, + category: item.categoryName, + start: DateTime.fromSeconds(item.utcBeginDate).toUTC(), + stop: DateTime.fromSeconds(item.utcEndDate).toUTC() + }) + }) + + return programs + }, + async channels() { + const channels = await axios + .get('https://zapon.zapsi.net/ao/m/api/epg/channels') + .then(r => r.data.data) + .catch(console.log) + + return channels.map(item => { + return { + lang: 'pt', + site_id: item.id, + name: item.name + } + }) + } +} + +function parseItems(content) { + const data = JSON.parse(content) + + return data.data || [] +} diff --git a/sites/zap.co.ao/zap.co.ao.test.js b/sites/zap.co.ao/zap.co.ao.test.js index 7331a16d..5481dd6d 100644 --- a/sites/zap.co.ao/zap.co.ao.test.js +++ b/sites/zap.co.ao/zap.co.ao.test.js @@ -1,48 +1,48 @@ -// npm run channels:parse -- --config=./sites/zap.co.ao/zap.co.ao.config.js --output=./sites/zap.co.ao/zap.co.ao.channels.xml -// npm run grab -- --site=zap.co.ao - -const { parser, url } = require('./zap.co.ao.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2023-05-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '2275', - xmltv_id: 'TPA1.ao' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://zapon.zapsi.net/ao/m/api/epg/events?date=20230528&channel=2275' - ) -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') - const results = parser({ content }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-05-27T23:00:00.000Z', - stop: '2023-05-28T00:00:00.000Z', - title: 'Jornal da Meia-Noite', - description: - 'Um jornal diferente do Telejornal, por conter análise, comentários e coluna com jornalistas experientes sobre factos do dia a dia.', - category: 'Noticiário' - }) -}) - -it('can handle empty guide', () => { - const results = parser({ - content: '[]' - }) - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/zap.co.ao/zap.co.ao.config.js --output=./sites/zap.co.ao/zap.co.ao.channels.xml +// npm run grab -- --site=zap.co.ao + +const { parser, url } = require('./zap.co.ao.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2023-05-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: '2275', + xmltv_id: 'TPA1.ao' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://zapon.zapsi.net/ao/m/api/epg/events?date=20230528&channel=2275' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') + const results = parser({ content }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-05-27T23:00:00.000Z', + stop: '2023-05-28T00:00:00.000Z', + title: 'Jornal da Meia-Noite', + description: + 'Um jornal diferente do Telejornal, por conter análise, comentários e coluna com jornalistas experientes sobre factos do dia a dia.', + category: 'Noticiário' + }) +}) + +it('can handle empty guide', () => { + const results = parser({ + content: '[]' + }) + expect(results).toMatchObject([]) +}) diff --git a/sites/ziggogo.tv/ziggogo.tv.config.js b/sites/ziggogo.tv/ziggogo.tv.config.js index 3fb147c6..42e171be 100644 --- a/sites/ziggogo.tv/ziggogo.tv.config.js +++ b/sites/ziggogo.tv/ziggogo.tv.config.js @@ -1,133 +1,133 @@ -const axios = require('axios') -const dayjs = require('dayjs') - -const API_ENDPOINT = 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite' - -module.exports = { - site: 'ziggogo.tv', - days: 2, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url: function ({ date, channel }) { - return `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` - }, - async parser({ content, channel, date }) { - let programs = [] - let items = parseItems(content, channel) - if (!items.length) return programs - const promises = [ - axios.get( - `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date - .add(6, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date - .add(12, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ), - axios.get( - `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date - .add(18, 'h') - .format('YYYYMMDDHHmmss')}`, - { - responseType: 'arraybuffer' - } - ) - ] - - await Promise.allSettled(promises) - .then(results => { - results.forEach(r => { - if (r.status === 'fulfilled') { - const parsed = parseItems(r.value.data, channel) - - items = items.concat(parsed) - } - }) - }) - .catch(console.error) - - for (let item of items) { - const detail = await loadProgramDetails(item, channel) - programs.push({ - title: item.title, - description: detail.longDescription, - category: detail.genres, - actors: detail.actors, - season: parseSeason(detail), - episode: parseEpisode(detail), - start: parseStart(item), - stop: parseStop(item) - }) - } - - return programs - }, - async channels() { - const data = await axios - .get( - 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/channels?cityId=65535&language=en&productClass=Orion-DASH' - ) - .then(r => r.data) - .catch(console.log) - - return data.channels.map(item => { - return { - lang: 'be', - site_id: item.id, - name: item.name - } - }) - } -} - -async function loadProgramDetails(item, channel) { - if (!item.id) return {} - const url = `https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` - const data = await axios - .get(url) - .then(r => r.data) - .catch(console.log) - - return data || {} -} - -function parseStart(item) { - return dayjs.unix(item.startTime) -} - -function parseStop(item) { - return dayjs.unix(item.endTime) -} - -function parseItems(content, channel) { - if (!content) return [] - const data = JSON.parse(content) - if (!data || !Array.isArray(data.entries)) return [] - const channelData = data.entries.find(e => e.channelId === channel.site_id) - if (!channelData) return [] - - return Array.isArray(channelData.events) ? channelData.events : [] -} - -function parseSeason(detail) { - if (!detail.seasonNumber) return null - if (String(detail.seasonNumber).length > 2) return null - return detail.seasonNumber -} - -function parseEpisode(detail) { - if (!detail.episodeNumber) return null - if (String(detail.episodeNumber).length > 3) return null - return detail.episodeNumber -} +const axios = require('axios') +const dayjs = require('dayjs') + +const API_ENDPOINT = 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite' + +module.exports = { + site: 'ziggogo.tv', + days: 2, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url: function ({ date, channel }) { + return `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date.format('YYYYMMDDHHmmss')}` + }, + async parser({ content, channel, date }) { + let programs = [] + let items = parseItems(content, channel) + if (!items.length) return programs + const promises = [ + axios.get( + `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date + .add(6, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date + .add(12, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ), + axios.get( + `${API_ENDPOINT}/nl/${channel.lang}/events/segments/${date + .add(18, 'h') + .format('YYYYMMDDHHmmss')}`, + { + responseType: 'arraybuffer' + } + ) + ] + + await Promise.allSettled(promises) + .then(results => { + results.forEach(r => { + if (r.status === 'fulfilled') { + const parsed = parseItems(r.value.data, channel) + + items = items.concat(parsed) + } + }) + }) + .catch(console.error) + + for (let item of items) { + const detail = await loadProgramDetails(item, channel) + programs.push({ + title: item.title, + description: detail.longDescription, + category: detail.genres, + actors: detail.actors, + season: parseSeason(detail), + episode: parseEpisode(detail), + start: parseStart(item), + stop: parseStop(item) + }) + } + + return programs + }, + async channels() { + const data = await axios + .get( + 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/channels?cityId=65535&language=en&productClass=Orion-DASH' + ) + .then(r => r.data) + .catch(console.log) + + return data.channels.map(item => { + return { + lang: 'be', + site_id: item.id, + name: item.name + } + }) + } +} + +async function loadProgramDetails(item, channel) { + if (!item.id) return {} + const url = `https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/${item.id}?returnLinearContent=true&language=${channel.lang}` + const data = await axios + .get(url) + .then(r => r.data) + .catch(console.log) + + return data || {} +} + +function parseStart(item) { + return dayjs.unix(item.startTime) +} + +function parseStop(item) { + return dayjs.unix(item.endTime) +} + +function parseItems(content, channel) { + if (!content) return [] + const data = JSON.parse(content) + if (!data || !Array.isArray(data.entries)) return [] + const channelData = data.entries.find(e => e.channelId === channel.site_id) + if (!channelData) return [] + + return Array.isArray(channelData.events) ? channelData.events : [] +} + +function parseSeason(detail) { + if (!detail.seasonNumber) return null + if (String(detail.seasonNumber).length > 2) return null + return detail.seasonNumber +} + +function parseEpisode(detail) { + if (!detail.episodeNumber) return null + if (String(detail.episodeNumber).length > 3) return null + return detail.episodeNumber +} diff --git a/sites/ziggogo.tv/ziggogo.tv.test.js b/sites/ziggogo.tv/ziggogo.tv.test.js index 502f46ec..0ec9c570 100644 --- a/sites/ziggogo.tv/ziggogo.tv.test.js +++ b/sites/ziggogo.tv/ziggogo.tv.test.js @@ -1,98 +1,98 @@ -// npm run channels:parse -- --config=./sites/ziggogo.tv/ziggogo.tv.config.js --output=./sites/ziggogo.tv/ziggogo.tv.channels.xml -// npm run grab -- --site=ziggogo.tv - -const { parser, url } = require('./ziggogo.tv.config.js') -const fs = require('fs') -const path = require('path') -const axios = require('axios') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2022-10-28', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'NL_000001_019401', - xmltv_id: 'NPO1.nl', - lang: 'nl' -} - -it('can generate valid url', () => { - expect(url({ date, channel })).toBe( - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028000000' - ) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) - - axios.get.mockImplementation(url => { - if ( - url === - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028060000' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) - }) - } else if ( - url === - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028120000' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) - }) - } else if ( - url === - 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028180000' - ) { - return Promise.resolve({ - data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) - }) - } else if ( - url === - 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F817615~~2FSH010806510000~~2F144222201,imi:ea187e3432c4a98b5ea45bcc5525c7a93c77b47b?returnLinearContent=true&language=nl' - ) { - return Promise.resolve({ - data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) - }) - } else { - return Promise.resolve({ data: '' }) - } - }) - - let results = await parser({ content, channel, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2022-10-27T23:40:00.000Z', - stop: '2022-10-28T00:07:00.000Z', - title: 'NOS Journaal', - description: - 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor de avond en komende dagen.', - category: ['Nieuws'], - actors: [ - 'Malou Petter', - 'Mark Visser', - 'Rob Trip', - 'Jeroen Overbeek', - 'Simone Weimans', - 'Annechien Steenhuizen', - 'Jeroen Tjepkema', - 'Saïda Maggé', - 'Winfried Baijens' - ] - }) -}) - -it('can handle empty guide', async () => { - let results = await parser({ content: '', channel, date }) - - expect(results).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/ziggogo.tv/ziggogo.tv.config.js --output=./sites/ziggogo.tv/ziggogo.tv.channels.xml +// npm run grab -- --site=ziggogo.tv + +const { parser, url } = require('./ziggogo.tv.config.js') +const fs = require('fs') +const path = require('path') +const axios = require('axios') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2022-10-28', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'NL_000001_019401', + xmltv_id: 'NPO1.nl', + lang: 'nl' +} + +it('can generate valid url', () => { + expect(url({ date, channel })).toBe( + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028000000' + ) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content_0000.json')) + + axios.get.mockImplementation(url => { + if ( + url === + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028060000' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_0600.json')) + }) + } else if ( + url === + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028120000' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1200.json')) + }) + } else if ( + url === + 'https://static.spark.ziggogo.tv/eng/web/epg-service-lite/nl/nl/events/segments/20221028180000' + ) { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/content_1800.json')) + }) + } else if ( + url === + 'https://prod.spark.ziggogo.tv/eng/web/linear-service/v2/replayEvent/crid:~~2F~~2Fgn.tv~~2F817615~~2FSH010806510000~~2F144222201,imi:ea187e3432c4a98b5ea45bcc5525c7a93c77b47b?returnLinearContent=true&language=nl' + ) { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } + }) + + let results = await parser({ content, channel, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2022-10-27T23:40:00.000Z', + stop: '2022-10-28T00:07:00.000Z', + title: 'NOS Journaal', + description: + 'Met het laatste nieuws, gebeurtenissen van nationaal en internationaal belang en de weersverwachting voor de avond en komende dagen.', + category: ['Nieuws'], + actors: [ + 'Malou Petter', + 'Mark Visser', + 'Rob Trip', + 'Jeroen Overbeek', + 'Simone Weimans', + 'Annechien Steenhuizen', + 'Jeroen Tjepkema', + 'Saïda Maggé', + 'Winfried Baijens' + ] + }) +}) + +it('can handle empty guide', async () => { + let results = await parser({ content: '', channel, date }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/znbc.co.zm/znbc.co.zm.config.js b/sites/znbc.co.zm/znbc.co.zm.config.js index 555d51e0..3d020e88 100644 --- a/sites/znbc.co.zm/znbc.co.zm.config.js +++ b/sites/znbc.co.zm/znbc.co.zm.config.js @@ -1,65 +1,65 @@ -const cheerio = require('cheerio') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const tabletojson = require('tabletojson').Tabletojson - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'znbc.co.zm', - days: 2, - url({ channel }) { - return `https://www.znbc.co.zm/${channel.site_id}/` - }, - parser({ content, date }) { - const programs = [] - const items = parseItems(content, date) - items.forEach(item => { - const prev = programs[programs.length - 1] - let start = parseStart(item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ title: item.title, start, stop }) - }) - - return programs - } -} - -function parseStart(item, date) { - const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` - - return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Africa/Lusaka') -} - -function parseItems(content, date) { - const dayOfWeek = date.format('dddd').toUpperCase() - const $ = cheerio.load(content) - const table = $(`.elementor-tab-mobile-title:contains("${dayOfWeek}")`).next().html() - if (!table) return [] - const data = tabletojson.convert(table) - if (!Array.isArray(data) || !Array.isArray(data[0])) return [] - - return data[0] - .map(row => { - const [, time, title] = row['0'].replace(/\s\s/g, ' ').match(/^(\d{2}:\d{2}) (.*)/) || [ - null, - null, - null - ] - if (!time || !title.trim()) return null - - return { time, title: title.trim() } - }) - .filter(i => i) -} +const cheerio = require('cheerio') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const tabletojson = require('tabletojson').Tabletojson + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'znbc.co.zm', + days: 2, + url({ channel }) { + return `https://www.znbc.co.zm/${channel.site_id}/` + }, + parser({ content, date }) { + const programs = [] + const items = parseItems(content, date) + items.forEach(item => { + const prev = programs[programs.length - 1] + let start = parseStart(item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ title: item.title, start, stop }) + }) + + return programs + } +} + +function parseStart(item, date) { + const dateString = `${date.format('YYYY-MM-DD')} ${item.time}` + + return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Africa/Lusaka') +} + +function parseItems(content, date) { + const dayOfWeek = date.format('dddd').toUpperCase() + const $ = cheerio.load(content) + const table = $(`.elementor-tab-mobile-title:contains("${dayOfWeek}")`).next().html() + if (!table) return [] + const data = tabletojson.convert(table) + if (!Array.isArray(data) || !Array.isArray(data[0])) return [] + + return data[0] + .map(row => { + const [, time, title] = row['0'].replace(/\s\s/g, ' ').match(/^(\d{2}:\d{2}) (.*)/) || [ + null, + null, + null + ] + if (!time || !title.trim()) return null + + return { time, title: title.trim() } + }) + .filter(i => i) +} diff --git a/sites/znbc.co.zm/znbc.co.zm.test.js b/sites/znbc.co.zm/znbc.co.zm.test.js index b8380dd4..f5d8185f 100644 --- a/sites/znbc.co.zm/znbc.co.zm.test.js +++ b/sites/znbc.co.zm/znbc.co.zm.test.js @@ -1,55 +1,55 @@ -// npm run grab -- --site=znbc.co.zm - -const { parser, url } = require('./znbc.co.zm.config.js') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tv1', - xmltv_id: 'ZNBCTV1.zm' -} -const content = - '
        ' - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.znbc.co.zm/tv1/') -}) - -it('can parse response', () => { - const result = parser({ content, channel, date }).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(result).toMatchObject([ - { - start: '2021-11-24T22:00:00.000Z', - stop: '2021-11-24T23:00:00.000Z', - title: 'MAIN NEWS – RPT' - }, - { - start: '2021-11-24T23:00:00.000Z', - stop: '2021-11-25T00:00:00.000Z', - title: 'BORN & BRED – Rebroadcast (Tuesday Edition)' - }, - { - start: '2021-11-25T00:00:00.000Z', - stop: '2021-11-25T00:30:00.000Z', - title: 'DOCUMENTARY – DW' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ - date, - channel, - content: '' - }) - expect(result).toMatchObject([]) -}) +// npm run grab -- --site=znbc.co.zm + +const { parser, url } = require('./znbc.co.zm.config.js') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2021-11-25', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tv1', + xmltv_id: 'ZNBCTV1.zm' +} +const content = + '
        ' + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.znbc.co.zm/tv1/') +}) + +it('can parse response', () => { + const result = parser({ content, channel, date }).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(result).toMatchObject([ + { + start: '2021-11-24T22:00:00.000Z', + stop: '2021-11-24T23:00:00.000Z', + title: 'MAIN NEWS – RPT' + }, + { + start: '2021-11-24T23:00:00.000Z', + stop: '2021-11-25T00:00:00.000Z', + title: 'BORN & BRED – Rebroadcast (Tuesday Edition)' + }, + { + start: '2021-11-25T00:00:00.000Z', + stop: '2021-11-25T00:30:00.000Z', + title: 'DOCUMENTARY – DW' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ + date, + channel, + content: '' + }) + expect(result).toMatchObject([]) +}) diff --git a/sites/zuragt.mn/zuragt.mn.config.js b/sites/zuragt.mn/zuragt.mn.config.js index ed197342..e20681b4 100644 --- a/sites/zuragt.mn/zuragt.mn.config.js +++ b/sites/zuragt.mn/zuragt.mn.config.js @@ -1,89 +1,89 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const cheerio = require('cheerio') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) - -module.exports = { - site: 'zuragt.mn', - days: 2, - url: function ({ channel, date }) { - return `https://m.zuragt.mn/channel/${channel.site_id}/?date=${date.format('YYYY-MM-DD')}` - }, - request: { - maxRedirects: 0, - validateStatus: function (status) { - return status >= 200 && status < 303 - } - }, - parser: async function ({ content, date }) { - let programs = [] - const items = parseItems(content) - for (let item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - let start = parseStart($item, date) - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - const stop = start.add(30, 'm') - programs.push({ - title: parseTitle($item), - start, - stop - }) - } - - return programs - }, - async channels() { - let html = await axios - .get('https://www.zuragt.mn/') - .then(r => r.data) - .catch(console.log) - let $ = cheerio.load(html) - - const items = $('.tv-box > ul > li').toArray() - return items - .map(item => { - const name = $(item).text().trim() - const link = $(item).find('a').attr('href') - - if (!link) return null - - const [, site_id] = link.match(/\/channel\/(.*)\//) || [null, null] - - return { - lang: 'mn', - site_id, - name - } - }) - .filter(Boolean) - } -} - -function parseTitle($item) { - return $item('.program').text().trim() -} - -function parseStart($item, date) { - const time = $item('.time') - - return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Ulaanbaatar') -} - -function parseItems(content) { - const $ = cheerio.load(content) - - return $('body > div > div > div > ul > li').toArray() -} +const dayjs = require('dayjs') +const axios = require('axios') +const cheerio = require('cheerio') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) + +module.exports = { + site: 'zuragt.mn', + days: 2, + url: function ({ channel, date }) { + return `https://m.zuragt.mn/channel/${channel.site_id}/?date=${date.format('YYYY-MM-DD')}` + }, + request: { + maxRedirects: 0, + validateStatus: function (status) { + return status >= 200 && status < 303 + } + }, + parser: async function ({ content, date }) { + let programs = [] + const items = parseItems(content) + for (let item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + let start = parseStart($item, date) + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + const stop = start.add(30, 'm') + programs.push({ + title: parseTitle($item), + start, + stop + }) + } + + return programs + }, + async channels() { + let html = await axios + .get('https://www.zuragt.mn/') + .then(r => r.data) + .catch(console.log) + let $ = cheerio.load(html) + + const items = $('.tv-box > ul > li').toArray() + return items + .map(item => { + const name = $(item).text().trim() + const link = $(item).find('a').attr('href') + + if (!link) return null + + const [, site_id] = link.match(/\/channel\/(.*)\//) || [null, null] + + return { + lang: 'mn', + site_id, + name + } + }) + .filter(Boolean) + } +} + +function parseTitle($item) { + return $item('.program').text().trim() +} + +function parseStart($item, date) { + const time = $item('.time') + + return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Ulaanbaatar') +} + +function parseItems(content) { + const $ = cheerio.load(content) + + return $('body > div > div > div > ul > li').toArray() +} diff --git a/sites/zuragt.mn/zuragt.mn.test.js b/sites/zuragt.mn/zuragt.mn.test.js index 9c9be992..1d8d75a2 100644 --- a/sites/zuragt.mn/zuragt.mn.test.js +++ b/sites/zuragt.mn/zuragt.mn.test.js @@ -1,49 +1,49 @@ -// npm run channels:parse -- --config=./sites/zuragt.mn/zuragt.mn.config.js --output=./sites/zuragt.mn/zuragt.mn.channels.xml -// npm run grab -- --site=zuragt.mn - -const { parser, url, request } = require('./zuragt.mn.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -jest.mock('axios') - -const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'mnb', - xmltv_id: 'MNB.mn' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://m.zuragt.mn/channel/mnb/?date=2023-01-15') -}) - -it('can generate valid request object', () => { - expect(request.maxRedirects).toBe(0) - expect(request.validateStatus(302)).toBe(true) -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results[0]).toMatchObject({ - start: '2023-01-14T23:00:00.000Z', - stop: '2023-01-15T00:00:00.000Z', - title: '“Цагийн хүрд” мэдээллийн хөтөлбөр' - }) -}) - -it('can handle empty guide', async () => { - const result = await parser({ content: '' }) - expect(result).toMatchObject([]) -}) +// npm run channels:parse -- --config=./sites/zuragt.mn/zuragt.mn.config.js --output=./sites/zuragt.mn/zuragt.mn.channels.xml +// npm run grab -- --site=zuragt.mn + +const { parser, url, request } = require('./zuragt.mn.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +jest.mock('axios') + +const date = dayjs.utc('2023-01-15', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'mnb', + xmltv_id: 'MNB.mn' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://m.zuragt.mn/channel/mnb/?date=2023-01-15') +}) + +it('can generate valid request object', () => { + expect(request.maxRedirects).toBe(0) + expect(request.validateStatus(302)).toBe(true) +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results[0]).toMatchObject({ + start: '2023-01-14T23:00:00.000Z', + stop: '2023-01-15T00:00:00.000Z', + title: '“Цагийн хүрд” мэдээллийн хөтөлбөр' + }) +}) + +it('can handle empty guide', async () => { + const result = await parser({ content: '' }) + expect(result).toMatchObject([]) +}) diff --git a/tests/__data__/input/channels-parse/channels-parse.config.js b/tests/__data__/input/channels-parse/channels-parse.config.js index 4688835f..6829ec21 100644 --- a/tests/__data__/input/channels-parse/channels-parse.config.js +++ b/tests/__data__/input/channels-parse/channels-parse.config.js @@ -1,21 +1,21 @@ -module.exports = { - site: 'parse-channels.com', - url: 'https://parse-channels.com', - parser() { - return [] - }, - channels() { - return [ - { - lang: 'en', - site_id: 140, - name: 'CNN International' - }, - { - lang: 'en', - site_id: 240, - name: 'BBC World News' - } - ] - } -} +module.exports = { + site: 'parse-channels.com', + url: 'https://parse-channels.com', + parser() { + return [] + }, + channels() { + return [ + { + lang: 'en', + site_id: 140, + name: 'CNN International' + }, + { + lang: 'en', + site_id: 240, + name: 'BBC World News' + } + ] + } +} diff --git a/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js b/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js index 51b43a62..7f65c30c 100644 --- a/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js +++ b/tests/__data__/input/epg-grab/sites/example.com/example.com.config.js @@ -1,28 +1,28 @@ -module.exports = { - site: 'example.com', - days: 2, - request: { - timeout: 1000 - }, - url: 'https://example.com', - parser({ channel, date }) { - if (channel.xmltv_id === 'Channel2.us') return [] - else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { - return [ - { - title: 'Programme1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } - - return [ - { - title: 'Program1 (example.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } -} +module.exports = { + site: 'example.com', + days: 2, + request: { + timeout: 1000 + }, + url: 'https://example.com', + parser({ channel, date }) { + if (channel.xmltv_id === 'Channel2.us') return [] + else if (channel.xmltv_id === 'Channel1.us' && channel.lang === 'fr') { + return [ + { + title: 'Programme1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } + + return [ + { + title: 'Program1 (example.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } +} diff --git a/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js b/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js index 68561cc8..8789ccdc 100644 --- a/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js +++ b/tests/__data__/input/epg-grab/sites/example2.com/example2.com.config.js @@ -1,23 +1,23 @@ -module.exports = { - site: 'example2.com', - url: 'https://example2.com', - parser({ channel, date }) { - if (channel.lang === 'fr') { - return [ - { - title: 'Programme1 (example2.com)', - start: `${date.format('YYYY-MM-DD')}T04:40:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } - - return [ - { - title: 'Program1 (example2.com)', - start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, - stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` - } - ] - } -} +module.exports = { + site: 'example2.com', + url: 'https://example2.com', + parser({ channel, date }) { + if (channel.lang === 'fr') { + return [ + { + title: 'Programme1 (example2.com)', + start: `${date.format('YYYY-MM-DD')}T04:40:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } + + return [ + { + title: 'Program1 (example2.com)', + start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, + stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` + } + ] + } +} diff --git a/tests/commands/channels/editor.test.ts b/tests/commands/channels/editor.test.ts index 36c2f16c..6abc50a7 100644 --- a/tests/commands/channels/editor.test.ts +++ b/tests/commands/channels/editor.test.ts @@ -1,51 +1,51 @@ -import fs from 'fs-extra' -import { execSync } from 'child_process' -import os from 'os' -import path from 'path' - -type ExecError = { - status: number - stdout: string -} - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') - fs.copySync( - 'tests/__data__/input/channels-editor/channels-editor.channels.xml', - 'tests/__data__/output/channels.xml' - ) -}) - -describe('channels:editor', () => { - it('shows list of options for a channel', () => { - let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' - if (os.platform() === 'win32') { - ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' - } - - try { - const cmd = `${ENV_VAR} npm run channels:editor -- tests/__data__/output/channels.xml` - execSync(cmd, { encoding: 'utf8' }) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain('CNN International | CNNInternational.us [new]') - expect((error as ExecError).stdout).toContain( - 'CNN International Europe | CNNInternationalEurope.us [api]' - ) - expect((error as ExecError).stdout).toContain('Overwrite') - expect((error as ExecError).stdout).toContain('Skip') - expect((error as ExecError).stdout).toContain( - "File 'tests/__data__/output/channels.xml' successfully saved" - ) - expect(content('tests/__data__/output/channels.xml')).toEqual( - content('tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml') - ) - } - }) -}) - -function content(filepath: string) { - return fs.readFileSync(path.resolve(filepath), { - encoding: 'utf8' - }) -} +import fs from 'fs-extra' +import { execSync } from 'child_process' +import os from 'os' +import path from 'path' + +type ExecError = { + status: number + stdout: string +} + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync( + 'tests/__data__/input/channels-editor/channels-editor.channels.xml', + 'tests/__data__/output/channels.xml' + ) +}) + +describe('channels:editor', () => { + it('shows list of options for a channel', () => { + let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' + if (os.platform() === 'win32') { + ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' + } + + try { + const cmd = `${ENV_VAR} npm run channels:editor -- tests/__data__/output/channels.xml` + execSync(cmd, { encoding: 'utf8' }) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain('CNN International | CNNInternational.us [new]') + expect((error as ExecError).stdout).toContain( + 'CNN International Europe | CNNInternationalEurope.us [api]' + ) + expect((error as ExecError).stdout).toContain('Overwrite') + expect((error as ExecError).stdout).toContain('Skip') + expect((error as ExecError).stdout).toContain( + "File 'tests/__data__/output/channels.xml' successfully saved" + ) + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-editor/channels-editor.channels.xml') + ) + } + }) +}) + +function content(filepath: string) { + return fs.readFileSync(path.resolve(filepath), { + encoding: 'utf8' + }) +} diff --git a/tests/commands/channels/lint.test.ts b/tests/commands/channels/lint.test.ts index 043c6e1b..2a772fe5 100644 --- a/tests/commands/channels/lint.test.ts +++ b/tests/commands/channels/lint.test.ts @@ -1,22 +1,22 @@ -import { execSync } from 'child_process' - -type ExecError = { - status: number - stdout: string -} - -describe('channels:lint', () => { - it('will show a message if the file contains a syntax error', () => { - try { - const cmd = - 'npm run channels:lint -- --channels=tests/__data__/input/channels-lint/channels-lint.channels.xml' - execSync(cmd, { encoding: 'utf8' }) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain( - "channels-lint.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" - ) - } - }) -}) +import { execSync } from 'child_process' + +type ExecError = { + status: number + stdout: string +} + +describe('channels:lint', () => { + it('will show a message if the file contains a syntax error', () => { + try { + const cmd = + 'npm run channels:lint -- --channels=tests/__data__/input/channels-lint/channels-lint.channels.xml' + execSync(cmd, { encoding: 'utf8' }) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain( + "channels-lint.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n" + ) + } + }) +}) diff --git a/tests/commands/channels/parse.test.ts b/tests/commands/channels/parse.test.ts index 83d91d7a..b50dda47 100644 --- a/tests/commands/channels/parse.test.ts +++ b/tests/commands/channels/parse.test.ts @@ -1,39 +1,39 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import path from 'path' - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') - fs.copySync( - 'tests/__data__/input/channels-parse/channels-parse.channels.xml', - 'tests/__data__/output/channels.xml' - ) -}) - -describe('channels:parse', () => { - it('can parse channels', () => { - const cmd = - 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml' - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/channels.xml')).toEqual( - content('tests/__data__/expected/sites/channels-parse/channels-parse.channels.xml') - ) - }) - - it('can parse channels with clean flag', () => { - const cmd = - 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml --clean' - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/channels.xml')).toEqual( - content('tests/__data__/expected/sites/channels-parse/channels-parse-clean.channels.xml') - ) - }) -}) - -function content(filepath: string) { - return fs.readFileSync(path.resolve(filepath), { - encoding: 'utf8' - }) -} +import { execSync } from 'child_process' +import fs from 'fs-extra' +import path from 'path' + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync( + 'tests/__data__/input/channels-parse/channels-parse.channels.xml', + 'tests/__data__/output/channels.xml' + ) +}) + +describe('channels:parse', () => { + it('can parse channels', () => { + const cmd = + 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml' + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-parse/channels-parse.channels.xml') + ) + }) + + it('can parse channels with clean flag', () => { + const cmd = + 'npm run channels:parse -- --config=tests/__data__/input/channels-parse/channels-parse.config.js --output=tests/__data__/output/channels.xml --clean' + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/channels.xml')).toEqual( + content('tests/__data__/expected/sites/channels-parse/channels-parse-clean.channels.xml') + ) + }) +}) + +function content(filepath: string) { + return fs.readFileSync(path.resolve(filepath), { + encoding: 'utf8' + }) +} diff --git a/tests/commands/channels/validate.test.ts b/tests/commands/channels/validate.test.ts index 3a305051..0177a6e5 100644 --- a/tests/commands/channels/validate.test.ts +++ b/tests/commands/channels/validate.test.ts @@ -1,48 +1,48 @@ -import { execSync } from 'child_process' -import os from 'os' - -type ExecError = { - status: number - stdout: string -} - -let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' -if (os.platform() === 'win32') { - ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' -} - -describe('channels:validate', () => { - it('will show a message if the file contains a duplicate', () => { - try { - const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/duplicate.channels.xml` - execSync(cmd, { encoding: 'utf8' }) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain(`duplicate.channels.xml -┌─────────┬─────────────┬──────┬────────────────┬─────────┬─────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼─────────────┼──────┼────────────────┼─────────┼─────────┤ -│ 0 │ 'duplicate' │ 'en' │ 'BravoEast.us' │ '140' │ 'Bravo' │ -└─────────┴─────────────┴──────┴────────────────┴─────────┴─────────┘ -\n1 error(s) in 1 file(s)\n`) - } - }) - - it('will show a message if the file contains a channel with wrong xmltv_id', () => { - try { - const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/wrong_xmltv_id.channels.xml` - execSync(cmd, { encoding: 'utf8' }) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain(`wrong_xmltv_id.channels.xml -┌─────────┬──────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ -│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ -├─────────┼──────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ -│ 0 │ 'wrong_xmltv_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ -└─────────┴──────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ -\n1 error(s) in 1 file(s)\n`) - } - }) -}) +import { execSync } from 'child_process' +import os from 'os' + +type ExecError = { + status: number + stdout: string +} + +let ENV_VAR = 'DATA_DIR=tests/__data__/input/temp/data' +if (os.platform() === 'win32') { + ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/temp/data" &&' +} + +describe('channels:validate', () => { + it('will show a message if the file contains a duplicate', () => { + try { + const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/duplicate.channels.xml` + execSync(cmd, { encoding: 'utf8' }) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain(`duplicate.channels.xml +┌─────────┬─────────────┬──────┬────────────────┬─────────┬─────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼─────────────┼──────┼────────────────┼─────────┼─────────┤ +│ 0 │ 'duplicate' │ 'en' │ 'BravoEast.us' │ '140' │ 'Bravo' │ +└─────────┴─────────────┴──────┴────────────────┴─────────┴─────────┘ +\n1 error(s) in 1 file(s)\n`) + } + }) + + it('will show a message if the file contains a channel with wrong xmltv_id', () => { + try { + const cmd = `${ENV_VAR} npm run channels:validate -- --channels=tests/__data__/input/channels-validate/wrong_xmltv_id.channels.xml` + execSync(cmd, { encoding: 'utf8' }) + process.exit(1) + } catch (error) { + expect((error as ExecError).status).toBe(1) + expect((error as ExecError).stdout).toContain(`wrong_xmltv_id.channels.xml +┌─────────┬──────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐ +│ (index) │ type │ lang │ xmltv_id │ site_id │ name │ +├─────────┼──────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤ +│ 0 │ 'wrong_xmltv_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ +└─────────┴──────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ +\n1 error(s) in 1 file(s)\n`) + } + }) +}) diff --git a/tests/commands/epg/grab.test.ts b/tests/commands/epg/grab.test.ts index 5d2b3b63..7c4ec98d 100644 --- a/tests/commands/epg/grab.test.ts +++ b/tests/commands/epg/grab.test.ts @@ -1,94 +1,94 @@ -import { execSync } from 'child_process' -import fs from 'fs-extra' -import path from 'path' -import os from 'os' -import { Zip } from '@freearhey/core' - -let ENV_VAR = - 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data' -if (os.platform() === 'win32') { - ENV_VAR = - 'SET "SITES_DIR=tests/__data__/input/epg-grab/sites" && SET "CURR_DATE=2022-10-20" && SET "DATA_DIR=tests/__data__/input/temp/data" &&' -} - -beforeEach(() => { - fs.emptyDirSync('tests/__data__/output') -}) - -describe('epg:grab', () => { - it('can grab epg by site name', () => { - const cmd = `${ENV_VAR} npm run grab -- --site=example.com --output=tests/__data__/output/guide.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide2.xml') - ) - }) - - it('can grab epg with multiple channels.xml files', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide.xml') - ) - }) - - it('can grab epg with gzip option enabled', async () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --gzip` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide.xml') - ) - - const zip = new Zip() - const expected = await zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) - const result = await zip.decompress(fs.readFileSync('tests/__data__/expected/guide.xml.gz')) - expect(expected).toEqual(result) - }) - - it('can grab epg with wildcard as output', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( - content('tests/__data__/expected/guides/en/example.com.xml') - ) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/guides/fr/example.com.xml') - ) - }) - - it('can grab epg then language filter enabled', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( - content('tests/__data__/expected/guides/fr/example.com.xml') - ) - }) - - it('can grab epg using custom channels list', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml` - execSync(cmd, { encoding: 'utf8' }) - - expect(content('tests/__data__/output/guide.xml')).toEqual( - content('tests/__data__/expected/guide.xml') - ) - }) - - it('it will raise an error if the timeout is exceeded', () => { - const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0` - const stdout = execSync(cmd, { encoding: 'utf8' }) - - expect(stdout).toContain('ERR: Connection timeout') - }) -}) - -function content(filepath: string) { - return fs.readFileSync(path.resolve(filepath), { - encoding: 'utf8' - }) -} +import { execSync } from 'child_process' +import fs from 'fs-extra' +import path from 'path' +import os from 'os' +import { Zip } from '@freearhey/core' + +let ENV_VAR = + 'SITES_DIR=tests/__data__/input/epg-grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/temp/data' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "SITES_DIR=tests/__data__/input/epg-grab/sites" && SET "CURR_DATE=2022-10-20" && SET "DATA_DIR=tests/__data__/input/temp/data" &&' +} + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') +}) + +describe('epg:grab', () => { + it('can grab epg by site name', () => { + const cmd = `${ENV_VAR} npm run grab -- --site=example.com --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide2.xml') + ) + }) + + it('can grab epg with multiple channels.xml files', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide.xml') + ) + }) + + it('can grab epg with gzip option enabled', async () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --gzip` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide.xml') + ) + + const zip = new Zip() + const expected = await zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz')) + const result = await zip.decompress(fs.readFileSync('tests/__data__/expected/guide.xml.gz')) + expect(expected).toEqual(result) + }) + + it('can grab epg with wildcard as output', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual( + content('tests/__data__/expected/guides/en/example.com.xml') + ) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/guides/fr/example.com.xml') + ) + }) + + it('can grab epg then language filter enabled', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual( + content('tests/__data__/expected/guides/fr/example.com.xml') + ) + }) + + it('can grab epg using custom channels list', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml` + execSync(cmd, { encoding: 'utf8' }) + + expect(content('tests/__data__/output/guide.xml')).toEqual( + content('tests/__data__/expected/guide.xml') + ) + }) + + it('it will raise an error if the timeout is exceeded', () => { + const cmd = `${ENV_VAR} npm run grab -- --channels=tests/__data__/input/epg-grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=0` + const stdout = execSync(cmd, { encoding: 'utf8' }) + + expect(stdout).toContain('ERR: Connection timeout') + }) +}) + +function content(filepath: string) { + return fs.readFileSync(path.resolve(filepath), { + encoding: 'utf8' + }) +}