Merge branch 'master' into pr/2823

This commit is contained in:
freearhey
2025-08-10 07:54:07 +03:00
665 changed files with 57693 additions and 50676 deletions
+2
View File
@@ -0,0 +1,2 @@
# Enforce the usage of CRLF in GitHub Actions per ESLint configuration.
* text eol=crlf
-7
View File
@@ -6,10 +6,3 @@
# macOS # macOS
.DS_Store .DS_Store
# If Yarn is used (yarn.lock)
/.yarn/*
.yarnrc
.yarnrc.yml
.idea
+5 -3
View File
@@ -1,4 +1,5 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin' import typescriptEslint from '@typescript-eslint/eslint-plugin'
import stylistic from '@stylistic/eslint-plugin'
import globals from 'globals' import globals from 'globals'
import tsParser from '@typescript-eslint/parser' import tsParser from '@typescript-eslint/parser'
import path from 'node:path' import path from 'node:path'
@@ -15,10 +16,11 @@ const compat = new FlatCompat({
}) })
export default [ export default [
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'), ...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/strict', 'plugin:@typescript-eslint/stylistic', 'prettier'),
{ {
plugins: { plugins: {
'@typescript-eslint': typescriptEslint '@typescript-eslint': typescriptEslint,
'@stylistic': stylistic
}, },
languageOptions: { languageOptions: {
@@ -36,7 +38,7 @@ export default [
'@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-var-requires': 'off',
'no-case-declarations': 'off', 'no-case-declarations': 'off',
'linebreak-style': ['error', 'windows'], '@stylistic/linebreak-style': ['error', 'windows'],
quotes: [ quotes: [
'error', 'error',
+2691 -1277
View File
File diff suppressed because it is too large Load Diff
+21 -13
View File
@@ -38,7 +38,7 @@
"dependencies": { "dependencies": {
"@alex_neo/jest-expect-message": "^1.0.5", "@alex_neo/jest-expect-message": "^1.0.5",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.30.0", "@eslint/js": "^9.32.0",
"@freearhey/chronos": "^0.0.1", "@freearhey/chronos": "^0.0.1",
"@freearhey/core": "^0.10.2", "@freearhey/core": "^0.10.2",
"@freearhey/search-js": "^0.1.2", "@freearhey/search-js": "^0.1.2",
@@ -46,33 +46,37 @@
"@octokit/core": "^7.0.3", "@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-rest": "^13.1.1", "@octokit/plugin-paginate-rest": "^13.1.1",
"@octokit/plugin-rest-endpoint-methods": "^16.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0",
"@swc/core": "^1.13.0", "@stylistic/eslint-plugin": "^5.2.2",
"@swc/core": "^1.13.2",
"@swc/jest": "^0.2.39", "@swc/jest": "^0.2.39",
"@types/cli-progress": "^3.11.6", "@types/cli-progress": "^3.11.6",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/inquirer": "^9.0.8", "@types/inquirer": "^9.0.8",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/langs": "^2.0.5", "@types/langs": "^2.0.5",
"@types/lodash": "^4.17.19", "@types/lodash.orderby": "^4.6.9",
"@types/node": "^24.0.14", "@types/lodash.sortby": "^4.7.9",
"@types/lodash.startcase": "^4.4.9",
"@types/lodash.uniqby": "^4.7.9",
"@types/node": "^24.1.0",
"@types/node-cleanup": "^2.1.5", "@types/node-cleanup": "^2.1.5",
"@types/numeral": "^2.0.5", "@types/numeral": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^8.35.0", "@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.35.0", "@typescript-eslint/parser": "^8.38.0",
"axios": "^1.10.0", "axios": "^1.11.0",
"axios-cookiejar-support": "^6.0.4", "axios-cookiejar-support": "^6.0.4",
"chalk": "^5.4.1", "chalk": "^5.4.1",
"cheerio": "^1.1.0", "cheerio": "^1.1.2",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"commander": "^14.0.0", "commander": "^14.0.0",
"consola": "^3.4.2", "consola": "^3.4.2",
"cross-env": "^7.0.3", "cross-env": "^10.0.0",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"cwait": "^1.1.2", "cwait": "^1.1.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"epg-grabber": "^0.41.0", "epg-grabber": "^0.41.0",
"epg-parser": "^0.3.1", "epg-parser": "^0.3.1",
"eslint": "^9.30.0", "eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"form-data": "^4.0.4", "form-data": "^4.0.4",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
@@ -80,12 +84,15 @@
"globals": "^16.3.0", "globals": "^16.3.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"inquirer": "^12.7.0", "inquirer": "^12.8.2",
"jest": "^30.0.3", "jest": "^30.0.5",
"jest-offline": "^1.0.1", "jest-offline": "^1.0.1",
"langs": "^2.0.0", "langs": "^2.0.0",
"libxml2-wasm": "^0.5.0", "libxml2-wasm": "^0.5.0",
"lodash": "^4.17.21", "lodash.orderby": "^4.6.0",
"lodash.sortby": "^4.7.0",
"lodash.startcase": "^4.4.0",
"lodash.uniqby": "^4.7.0",
"luxon": "^3.7.1", "luxon": "^3.7.1",
"mockdate": "^3.0.5", "mockdate": "^3.0.5",
"nedb-promises": "^6.2.3", "nedb-promises": "^6.2.3",
@@ -110,6 +117,7 @@
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"unzipit": "^1.4.3", "unzipit": "^1.4.3",
"uuid": "^11.1.0",
"wildcard-match": "^5.1.4" "wildcard-match": "^5.1.4"
} }
} }
+2 -2
View File
@@ -13,8 +13,8 @@ import epgGrabber from 'epg-grabber'
import { Command } from 'commander' import { Command } from 'commander'
import readline from 'readline' import readline from 'readline'
type ChoiceValue = { type: string; value?: Feed | Channel } interface ChoiceValue { type: string; value?: Feed | Channel }
type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean } interface Choice { name: string; short?: string; value: ChoiceValue; default?: boolean }
if (process.platform === 'win32') { if (process.platform === 'win32') {
readline readline
+2 -4
View File
@@ -12,7 +12,7 @@ program
.option('-o, --output <output>', 'Output file') .option('-o, --output <output>', 'Output file')
.parse(process.argv) .parse(process.argv)
type ParseOptions = { interface ParseOptions {
config: string config: string
set?: string set?: string
output?: string output?: string
@@ -43,9 +43,7 @@ async function main() {
channelList = await parser.parse(outputFilepath) channelList = await parser.parse(outputFilepath)
} }
const args: { const args: Record<string, string> = {}
[key: string]: string
} = {}
if (Array.isArray(options.set)) { if (Array.isArray(options.set)) {
options.set.forEach((arg: string) => { options.set.forEach((arg: string) => {
+2 -2
View File
@@ -9,9 +9,9 @@ import { program } from 'commander'
import chalk from 'chalk' import chalk from 'chalk'
import langs from 'langs' import langs from 'langs'
program.argument('[filepath]', 'Path to *.channels.xml files to validate').parse(process.argv) program.argument('[filepath...]', 'Path to *.channels.xml files to validate').parse(process.argv)
type ValidationError = { interface ValidationError {
type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang' type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang'
name: string name: string
lang?: string lang?: string
+1 -1
View File
@@ -48,7 +48,7 @@ program
.addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL')) .addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL'))
.parse() .parse()
export type GrabOptions = { export interface GrabOptions {
site?: string site?: string
channels?: string channels?: string
output: string output: string
+1 -1
View File
@@ -2,7 +2,7 @@ import { parseChannels } from 'epg-grabber'
import { Storage } from '@freearhey/core' import { Storage } from '@freearhey/core'
import { ChannelList } from '../models' import { ChannelList } from '../models'
type ChannelsParserProps = { interface ChannelsParserProps {
storage: Storage storage: Storage
} }
+1 -2
View File
@@ -1,5 +1,4 @@
import { SiteConfig } from 'epg-grabber' import { SiteConfig } from 'epg-grabber'
import _ from 'lodash'
import { pathToFileURL } from 'url' import { pathToFileURL } from 'url'
export class ConfigLoader { export class ConfigLoader {
@@ -28,6 +27,6 @@ export class ConfigLoader {
channels: undefined channels: undefined
} }
return _.merge(defaultConfig, config) return { ...defaultConfig, ...config } as SiteConfig
} }
} }
-1
View File
@@ -3,7 +3,6 @@ import { DataLoaderData } from '../types/dataLoader'
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
export class DataProcessor { export class DataProcessor {
constructor() {}
process(data: DataLoaderData) { process(data: DataLoaderData) {
let channels = new Collection(data.channels).map(data => new Channel(data)) let channels = new Collection(data.channels).map(data => new Channel(data))
+1 -1
View File
@@ -5,7 +5,7 @@ import { GrabOptions } from '../commands/epg/grab'
import { TaskQueue, PromisyClass } from 'cwait' import { TaskQueue, PromisyClass } from 'cwait'
import { SocksProxyAgent } from 'socks-proxy-agent' import { SocksProxyAgent } from 'socks-proxy-agent'
type GrabberProps = { interface GrabberProps {
logger: Logger logger: Logger
queue: Queue queue: Queue
options: GrabOptions options: GrabOptions
+1 -1
View File
@@ -8,7 +8,7 @@ import { DataLoaderData } from '../types/dataLoader'
import { DataProcessorData } from '../types/dataProcessor' import { DataProcessorData } from '../types/dataProcessor'
import { DATA_DIR } from '../constants' import { DATA_DIR } from '../constants'
type GuideManagerProps = { interface GuideManagerProps {
options: OptionValues options: OptionValues
logger: Logger logger: Logger
channels: Collection channels: Collection
+1 -1
View File
@@ -1,4 +1,4 @@
type Column = { interface Column {
name: string name: string
nowrap?: boolean nowrap?: boolean
align?: string align?: string
+1 -1
View File
@@ -2,7 +2,7 @@ import { Logger } from '@freearhey/core'
import { Queue, Grabber, GuideManager } from '.' import { Queue, Grabber, GuideManager } from '.'
import { GrabOptions } from '../commands/epg/grab' import { GrabOptions } from '../commands/epg/grab'
type JobProps = { interface JobProps {
options: GrabOptions options: GrabOptions
logger: Logger logger: Logger
queue: Queue queue: Queue
+1 -1
View File
@@ -1,6 +1,6 @@
import { URL } from 'node:url' import { URL } from 'node:url'
type ProxyParserResult = { interface ProxyParserResult {
protocol: string | null protocol: string | null
auth?: { auth?: {
username?: string username?: string
+1 -1
View File
@@ -1,7 +1,7 @@
import { Dictionary } from '@freearhey/core' import { Dictionary } from '@freearhey/core'
import { SiteConfig, Channel } from 'epg-grabber' import { SiteConfig, Channel } from 'epg-grabber'
export type QueueItem = { export interface QueueItem {
channel: Channel channel: Channel
date: string date: string
config: SiteConfig config: SiteConfig
+1 -1
View File
@@ -5,7 +5,7 @@ import { ConfigLoader, Queue } from './'
import { SiteConfig } from 'epg-grabber' import { SiteConfig } from 'epg-grabber'
import path from 'path' import path from 'path'
type QueueCreatorProps = { interface QueueCreatorProps {
logger: Logger logger: Logger
options: GrabOptions options: GrabOptions
channels: Collection channels: Collection
+2 -2
View File
@@ -12,7 +12,7 @@ export class Channel {
subdivisionCode?: string subdivisionCode?: string
cityName?: string cityName?: string
categoryIds?: Collection categoryIds?: Collection
isNSFW: boolean = false isNSFW = false
launched?: string launched?: string
closed?: string closed?: string
replacedBy?: string replacedBy?: string
@@ -116,7 +116,7 @@ export class Channel {
} }
function format(logo: Logo): number { function format(logo: Logo): number {
const levelByFormat: { [key: string]: number } = { const levelByFormat: Record<string, number> = {
SVG: 0, SVG: 0,
PNG: 3, PNG: 3,
APNG: 1, APNG: 1,
+1 -1
View File
@@ -29,7 +29,7 @@ export class ChannelList {
} }
toString() { toString() {
function escapeString(value: string, defaultValue: string = '') { function escapeString(value: string, defaultValue = '') {
if (!value) return defaultValue if (!value) return defaultValue
const regex = new RegExp( const regex = new RegExp(
+1 -1
View File
@@ -87,7 +87,7 @@ export class Feed {
getLogos(): Collection { getLogos(): Collection {
function format(logo: Logo): number { function format(logo: Logo): number {
const levelByFormat: { [key: string]: number } = { const levelByFormat: Record<string, number> = {
SVG: 0, SVG: 0,
PNG: 3, PNG: 3,
APNG: 1, APNG: 1,
+1 -1
View File
@@ -1,7 +1,7 @@
import { Collection, DateTime } from '@freearhey/core' import { Collection, DateTime } from '@freearhey/core'
import { generateXMLTV } from 'epg-grabber' import { generateXMLTV } from 'epg-grabber'
type GuideData = { interface GuideData {
channels: Collection channels: Collection
programs: Collection programs: Collection
filepath: string filepath: string
+1 -1
View File
@@ -1,7 +1,7 @@
import { Dictionary } from '@freearhey/core' import { Dictionary } from '@freearhey/core'
import { OWNER, REPO } from '../constants' import { OWNER, REPO } from '../constants'
type IssueProps = { interface IssueProps {
number: number number: number
labels: string[] labels: string[]
data: Dictionary data: Dictionary
+2 -2
View File
@@ -7,8 +7,8 @@ export class Logo {
feedId?: string feedId?: string
feed?: Feed feed?: Feed
tags: Collection = new Collection() tags: Collection = new Collection()
width: number = 0 width = 0
height: number = 0 height = 0
format?: string format?: string
url?: string url?: string
+2 -2
View File
@@ -7,12 +7,12 @@ enum StatusCode {
OK = 'ok' OK = 'ok'
} }
type Status = { interface Status {
code: StatusCode code: StatusCode
emoji: string emoji: string
} }
type SiteProps = { interface SiteProps {
domain: string domain: string
totalChannels?: number totalChannels?: number
markedChannels?: number markedChannels?: number
+2 -2
View File
@@ -16,8 +16,8 @@ export class Stream {
isInterlaced?: boolean isInterlaced?: boolean
referrer?: string referrer?: string
userAgent?: string userAgent?: string
groupTitle: string = 'Undefined' groupTitle = 'Undefined'
removed: boolean = false removed = false
constructor(data: StreamData) { constructor(data: StreamData) {
const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel
+2 -2
View File
@@ -1,6 +1,6 @@
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
export type ChannelData = { export interface ChannelData {
id: string id: string
name: string name: string
alt_names: string[] alt_names: string[]
@@ -17,7 +17,7 @@ export type ChannelData = {
website: string website: string
} }
export type ChannelSearchableData = { export interface ChannelSearchableData {
id: string id: string
name: string name: string
altNames: string[] altNames: string[]
+2 -2
View File
@@ -1,10 +1,10 @@
import { Storage } from '@freearhey/core' import { Storage } from '@freearhey/core'
export type DataLoaderProps = { export interface DataLoaderProps {
storage: Storage storage: Storage
} }
export type DataLoaderData = { export interface DataLoaderData {
countries: object | object[] countries: object | object[]
regions: object | object[] regions: object | object[]
subdivisions: object | object[] subdivisions: object | object[]
+1 -1
View File
@@ -1,6 +1,6 @@
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
export type DataProcessorData = { export interface DataProcessorData {
guideChannelsGroupedByStreamId: Dictionary guideChannelsGroupedByStreamId: Dictionary
feedsGroupedByChannelId: Dictionary feedsGroupedByChannelId: Dictionary
logosGroupedByChannelId: Dictionary logosGroupedByChannelId: Dictionary
+1 -1
View File
@@ -1,6 +1,6 @@
import { Collection } from '@freearhey/core' import { Collection } from '@freearhey/core'
export type FeedData = { export interface FeedData {
channel: string channel: string
id: string id: string
name: string name: string
+1 -1
View File
@@ -1,4 +1,4 @@
export type GuideData = { export interface GuideData {
channel: string channel: string
feed: string feed: string
site: string site: string
+1 -1
View File
@@ -1,4 +1,4 @@
export type LogoData = { export interface LogoData {
channel: string channel: string
feed: string | null feed: string | null
tags: string[] tags: string[]
+1 -1
View File
@@ -1,4 +1,4 @@
export type StreamData = { export interface StreamData {
channel: string | null channel: string | null
feed: string | null feed: string | null
name?: string name?: string
+4 -3
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./9tv.co.il.config.js') const { parser, url } = require('./9tv.co.il.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -18,8 +20,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
'<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.&#160;</div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>'
const result = parser({ content, date }).map(p => { const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -49,7 +50,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+1
View File
@@ -0,0 +1 @@
<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.&#160;</div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><html><head></head><body></body></html>
+1
View File
@@ -0,0 +1 @@
{"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"}}]}]}
@@ -0,0 +1 @@
{"date":"2001-11-17","categories":[],"channels":[]}
+3 -2
View File
@@ -2,6 +2,8 @@ const { parser, url } = require('./allente.dk.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const { readFileSync } = require('fs')
const { resolve } = require('path')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = readFileSync(resolve(__dirname, '__data__/content.json'))
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
+1
View File
@@ -0,0 +1 @@
{"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"}}]}]}
@@ -0,0 +1 @@
{"date":"2001-11-17","categories":[],"channels":[]}
+4 -3
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./allente.fi.config.js') const { parser, url } = require('./allente.fi.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -44,7 +45,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+1
View File
@@ -0,0 +1 @@
{"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"}}]}]}
@@ -0,0 +1 @@
{"date":"2001-11-17","categories":[],"channels":[]}
+4 -3
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./allente.no.config.js') const { parser, url } = require('./allente.no.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -44,7 +45,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+1
View File
@@ -0,0 +1 @@
{"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"}}]}]}
@@ -0,0 +1 @@
{"date":"2001-11-17","categories":[],"channels":[]}
+4 -3
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./allente.se.config.js') const { parser, url } = require('./allente.se.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'{"channels":[{"id":"0148","icon":"//images.ctfassets.net/989y85n5kcxs/5uT9g9pdQWRZeDPQXVI9g6/9cc44da567f591822ed645c99ecdcb64/SVT_1_black_new__2_.png","name":"SVT1 HD (T)","events":[{"id":"0086202208220710","live":false,"time":"2022-08-22T07:10:00Z","title":"Hemmagympa med Sofia","details":{"title":"Hemmagympa med Sofia","image":"https://viasatps.api.comspace.se/PS/channeldate/image/viasat.ps/21/2022-08-22/se.cs.svt1.event.A_41214031600.jpg?size=2560x1440","description":"Svenskt träningsprogram från 2021. Styrka. Sofia Åhman leder SVT:s hemmagympapass. Denna gång fokuserar vi på styrka.","season":4,"episode":1,"categories":["other"],"duration":"20"}}]}]}'
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -44,7 +45,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '{"date":"2001-11-17","categories":[],"channels":[]}' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="arianaafgtv.com" lang="en" xmltv_id="ArianaAfghanistanInternationalTV.us" site_id="#">Ariana Afghanistan Television</channel>
</channels>
@@ -1,82 +0,0 @@
const cheerio = require('cheerio')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
module.exports = {
site: 'arianaafgtv.com',
days: 2,
url: 'https://www.arianaafgtv.com/index.html',
parser({ content, date }) {
const programs = []
const items = parseItems(content, date)
items.forEach(item => {
const title = item.title
const start = parseStart(item, date)
const stop = parseStop(item, date)
programs.push({
title,
start,
stop
})
})
return programs
}
}
function parseStop(item, date) {
const time = `${date.format('MM/DD/YYYY')} ${item.end.toUpperCase()}`
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
}
function parseStart(item, date) {
const time = `${date.format('MM/DD/YYYY')} ${item.start.toUpperCase()}`
return dayjs.tz(time, 'MM/DD/YYYY hh:mm A', 'Asia/Kabul')
}
function parseItems(content, date) {
const $ = cheerio.load(content)
const dayOfWeek = date.format('dddd')
const column = $('.H4')
.filter((i, el) => {
return $(el).text() === dayOfWeek
})
.first()
.parent()
const rows = column
.find('.Paragraph')
.map((i, el) => {
return $(el).html()
})
.toArray()
.map(r => (r === '&nbsp;' ? '|' : r))
.join(' ')
.split('|')
const items = []
rows.forEach(row => {
row = row.trim()
if (row) {
const found = row.match(/(\d+(|:\d+)(a|p)m-\d+(|:\d+)(a|p)m)/gi)
if (!found) return
const time = found[0]
let start = time.match(/(\d+(|:\d+)(a|p)m)-/i)[1]
start = dayjs(start.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
let end = time.match(/-(\d+(|:\d+)(a|p)m)/i)[1]
end = dayjs(end.toUpperCase(), ['hh:mmA', 'h:mmA', 'hA']).format('hh:mm A')
const title = row.replace(time, '').replace('&nbsp;', '').trim()
items.push({ start, end, title })
}
})
return items
}
-15
View File
@@ -1,15 +0,0 @@
# arianaafgtv.com
https://arianaafgtv.com/#ariana-afghanistan-television-tv-guide
### Download the guide
```sh
npm run grab --- --site=arianaafgtv.com
```
### Test
```sh
npm test --- arianaafgtv.com
```
@@ -0,0 +1 @@
<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>
@@ -0,0 +1 @@
<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>
@@ -1,4 +1,6 @@
const { parser, url } = require('./arianatelevision.com.config.js') const { parser, url } = require('./arianatelevision.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10">[[["Start","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","",""],["7:00","City Report","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","ICC T20 Highlights","",""],["7:30","ICC T20 Highlights","Sport ","Sport ","Sport ","Sport ","Sport ","Sport ","",""],["15:00","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","ICC T20 World Cup","",""],["6:30","Quran and Hadis ","Falah","Falah","Falah","Falah","Falah","Falah","",""],["","\\n","","","","","","","",""]]]</textarea></body></html>'
const result = parser({ content, date }).map(p => { const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -52,8 +53,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
'<!DOCTYPE html><html><head></head><body><textarea data-jtrt-table-id="508" id="jtrt_table_settings_508" cols="30" rows="10"></textarea></body></html>'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+1
View File
@@ -0,0 +1 @@
[{"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}]
+3 -2
View File
@@ -1,4 +1,6 @@
const { parser, url, request } = require('./artonline.tv.config.js') const { parser, url, request } = require('./artonline.tv.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -37,8 +39,7 @@ it('can generate valid request data for tomorrow', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'[{"id":158963,"eventid":null,"duration":"01:34:00","lang":"Arabic","title":"الراقصه و السياسي","description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","thumbnail":"/UploadImages/Channel/ARTAFLAM1/03/AlRaqesaWaAlSeyasi.jpg","image":"0","start_Time":"00:30","adddate":"3/4/2022 12:00:00 AM","repeat1":null,"iD_genre":0,"iD_Show_Type":0,"iD_Channel":77,"iD_country":0,"iD_rating":0,"end_time":"02:04","season_Number":0,"epoisode_Number":0,"hasCatchup":0,"cmsid":0,"containerID":0,"imagePath":"../../UploadImages/Channel/ARTAFLAM1/3/","youtube":"0","published_at":"0","directed_by":"0","composition":"0","cast":"0","timeShow":null,"short_description":"تقرر الراقصه سونيا انشاء دار حضانه للأطفال اليتامى و عندما تتقدم بمشورعها للمسئول يرفض فتتحداه ، تلجأ للوزير عبد الحميد رأفت تربطه بها علاقة قديمة ، يخشى على مركزه و يرفض مساعدتها فتقرر كتابة مذكراتها بمساعدة أحد الصحفيين ، يتخوف عبد الحميد و المسئولين ثم يفاجأ عبد الحميد بحصول سونيا على الموافقه للمشورع و البدء في تنفيذه و ذلك لعلاقتها بأحد كبار المسئولين .","seOdescription":null,"tagseo":null,"channel_name":null,"pathimage":null,"pathThumbnail":null}]'
const result = parser({ content }).map(p => { const result = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
+14
View File
@@ -0,0 +1,14 @@
<div class="pm0 col-md-8" id="stopfix">
<h2 class="posts-date">Programmes d'Aujourd'hui</h2>
<div class="posts-area">
<h2> <i class="fas fa-circle"></i>00:00 - 09:00</h2>
<div class="relativeme">
<a href="https://chada.ma/fr/emissions/bloc-prime-clips/">
<img class="programthumb" src="https://chada.ma/wp-content/uploads/2023/11/Autres-slides-clips-la-couverture.jpg">
</a>
</div>
<h3>Bloc Prime + Clips</h3>
<div class="authorbox"></div>
<div class="ssprogramme row"></div>
</div>
</div>
+1
View File
@@ -0,0 +1 @@
<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>
+4 -20
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./chada.ma.config.js') const { parser, url } = require('./chada.ma.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
@@ -10,30 +12,12 @@ dayjs.extend(customParseFormat)
jest.mock('axios') jest.mock('axios')
const mockHtmlContent = `
<div class="pm0 col-md-8" id="stopfix">
<h2 class="posts-date">Programmes d'Aujourd'hui</h2>
<div class="posts-area">
<h2> <i class="fas fa-circle"></i>00:00 - 09:00</h2>
<div class="relativeme">
<a href="https://chada.ma/fr/emissions/bloc-prime-clips/">
<img class="programthumb" src="https://chada.ma/wp-content/uploads/2023/11/Autres-slides-clips-la-couverture.jpg">
</a>
</div>
<h3>Bloc Prime + Clips</h3>
<div class="authorbox"></div>
<div class="ssprogramme row"></div>
</div>
</div>
`
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/') expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = mockHtmlContent const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content }).map(p => { const result = parser({ content }).map(p => {
p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') p.start = dayjs(p.start).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') p.stop = dayjs(p.stop).tz('Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
@@ -52,7 +36,7 @@ it('can parse response', () => {
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '<div class="pm0 col-md-8" id="stopfix"><div class="posts-area"></div></div>' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
@@ -0,0 +1 @@
{"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"}]}
@@ -0,0 +1 @@
{"code":60,"message":"Resource not found","param":{},"description":"L'URI demandé ou la ressource demandée n'existe pas.","stackTrace":null}
@@ -1,4 +1,6 @@
const { parser, url } = require('./chaines-tv.orange.fr.config.js') const { parser, url } = require('./chaines-tv.orange.fr.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -10,8 +12,6 @@ const channel = {
site_id: '192', site_id: '192',
xmltv_id: 'TF1.fr' 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', () => { it('can generate valid url', () => {
const result = url({ channel, date }) const result = url({ channel, date })
@@ -21,6 +21,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ date, channel, content }) const result = parser({ date, channel, content })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
@@ -42,8 +43,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
'{"code":60,"message":"Resource not found","param":{},"description":"L\'URI demandé ou la ressource demandée n\'existe pas.","stackTrace":null}'
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+20
View File
@@ -0,0 +1,20 @@
{
"channels": [
{
"items": [
{
"startTime": "2024-12-26T23:00:00+00:00",
"endTime": "2024-12-27T00:00:00+00:00",
"title": "Τι Λέει ο Νόμος",
"description": "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.",
"qoe": {
"genre": "Special"
},
"thumbnails": {
"standard": "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg"
}
}
]
}
]
}
@@ -0,0 +1 @@
{"date":"2024-12-26","categories":[],"channels":[]}
+4 -25
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./cosmotetv.gr.config.js') const { parser, url } = require('./cosmotetv.gr.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -13,29 +15,6 @@ jest.mock('axios')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' } const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
const mockEpgData = {
channels: [
{
items: [
{
startTime: '2024-12-26T23:00:00+00:00',
endTime: '2024-12-27T00:00:00+00:00',
title: 'Τι Λέει ο Νόμος',
description:
'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
qoe: {
genre: 'Special'
},
thumbnails: {
standard:
'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg'
}
}
]
}
]
}
it('can generate valid url', () => { it('can generate valid url', () => {
const startOfDay = dayjs(date).startOf('day').utc().unix() const startOfDay = dayjs(date).startOf('day').utc().unix()
const endOfDay = dayjs(date).endOf('day').utc().unix() const endOfDay = dayjs(date).endOf('day').utc().unix()
@@ -45,7 +24,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = JSON.stringify(mockEpgData) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const result = parser({ date, content }).map(p => { const result = parser({ date, content }).map(p => {
p.start = dayjs(p.start).toISOString() p.start = dayjs(p.start).toISOString()
p.stop = dayjs(p.stop).toISOString() p.stop = dayjs(p.stop).toISOString()
@@ -70,7 +49,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '{"date":"2024-12-26","categories":[],"channels":[]}' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'), 'utf8')
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+1
View File
@@ -0,0 +1 @@
{"result":[{"channel_id":"4028c68574537fcd0174be43042758d8","channel_name":"Trans TV","scehedule_title":"CNN Tech News","schedule_date":"2023-11-05 01:30:00","schedule_end_time":"02:00:00","schedule_json":{"availability":0,"channelId":"4028c68574537fcd0174be43042758d8","channelName":"Trans TV","duration":1800,"editable":true,"episodeName":"","imageUrl":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/458x640","imageUrlWide":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/320x180","name":"CNN Tech News","ottImageUrl":"","primarySynopsis":"CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.","scheduleId":"4028c6858b8b3621018b9330e3701a7e","scheduleTime":"18:30:00","secondarySynopsis":"CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.","startDt":"20231104183000","url":""},"schedule_start_time":"01:30:00"}]}
+3 -3
View File
@@ -1,4 +1,6 @@
const { url, parser } = require('./cubmu.com.config.js') const { url, parser } = require('./cubmu.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
@@ -14,9 +16,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
'{"result":[{"channel_id":"4028c68574537fcd0174be43042758d8","channel_name":"Trans TV","scehedule_title":"CNN Tech News","schedule_date":"2023-11-05 01:30:00","schedule_end_time":"02:00:00","schedule_json":{"availability":0,"channelId":"4028c68574537fcd0174be43042758d8","channelName":"Trans TV","duration":1800,"editable":true,"episodeName":"","imageUrl":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/458x640","imageUrlWide":"https://cdnjkt2.transvision.co.id:1001/catchup/schedule/thumbnail/4028c68574537fcd0174be43042758d8/4028c6858b8b3621018b9330e3701a7e/320x180","name":"CNN Tech News","ottImageUrl":"","primarySynopsis":"CNN Indonesia Tech News adalah berita teknologi yang membawa pemirsa ke dunia teknologi yang penuh dengan informasi, pendidikan, hiburan sampai informasi kesehatan terkini.","scheduleId":"4028c6858b8b3621018b9330e3701a7e","scheduleTime":"18:30:00","secondarySynopsis":"CNN Indonesia Tech News is tech news brings viewers into the world of technology that provides information, education, entertainment to the latest health information.","startDt":"20231104183000","url":""},"schedule_start_time":"01:30:00"}]}'
const idResults = parser({ content, channel }) const idResults = parser({ content, channel })
expect(idResults).toMatchObject([ expect(idResults).toMatchObject([
{ {
+2 -2
View File
@@ -3,7 +3,7 @@ const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const parseDuration = require('parse-duration').default const parseDuration = require('parse-duration').default
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const _ = require('lodash') const sortBy = require('lodash.sortby')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
@@ -28,7 +28,7 @@ module.exports = {
} }
}) })
return _.sortBy(programs, p => p.start) return sortBy(programs, p => p.start.valueOf())
} }
} }
@@ -0,0 +1 @@
{"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}]}]}
+3 -2
View File
@@ -1,4 +1,6 @@
const { parser, url, request } = require('./directv.com.ar.config.js') const { parser, url, request } = require('./directv.com.ar.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -45,8 +47,7 @@ it('can generate valid request data', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'{"d":[{"ChannelSection":"","ChannelFullName":"A&E HD","IsFavorite":false,"ChannelName":"A&EHD","ChannelNumber":207,"ProgramList":[{"_channelSection":"","eventId":"120289890767","titleId":"SH0110397700000001","title":"Chicas guapas","programId":null,"description":"Un espacio destinado a la belleza y los distintos estilos de vida, que muestra el trabajo inspiracional de la moda latinoamericana.","episodeTitle":null,"channelNumber":120,"channelName":"AME2","channelFullName":"América TV (ARG)","channelSection":"","contentChannelID":120,"startTime":"/Date(-62135578800000)/","endTime":"/Date(-62135578800000)/","GMTstartTime":"/Date(-62135578800000)/","GMTendTime":"/Date(-62135578800000)/","css":16,"language":null,"tmsId":"SH0110397700000001","rating":"NR","categoryId":"Tipos de Programas","categoryName":0,"subCategoryId":0,"subCategoryName":"Series","serviceExpiration":"/Date(-62135578800000)/","crId":null,"promoUrl1":null,"promoUrl2":null,"price":0,"isPurchasable":"N","videoUrl":"","imageUrl":"https://dnqt2wx2urq99.cloudfront.net/ondirectv/LOGOS/Canales/AR/120.png","titleSecond":"Chicas guapas","isHD":"N","DetailsURL":null,"BuyURL":null,"ProgramServiceId":null,"SearchDateTime":null,"startTimeString":"6/19/2022 12:00:00 AM","endTimeString":"6/19/2022 12:15:00 AM","DurationInMinutes":null,"castDetails":null,"scheduleDetails":null,"seriesDetails":null,"processedSeasonDetails":null}]}]}'
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
+2 -2
View File
@@ -3,6 +3,7 @@ const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const uniqBy = require('lodash.uniqby')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@@ -45,7 +46,6 @@ module.exports = {
return programs return programs
}, },
async channels({ country }) { async channels({ country }) {
const _ = require('lodash')
const countries = { const countries = {
ao: 'ago', ao: 'ago',
@@ -114,7 +114,7 @@ module.exports = {
}) })
}) })
return _.uniqBy(channels, 'site_id') return uniqBy(channels, 'site_id')
} }
} }
@@ -0,0 +1 @@
{"data":{"entries":{"243":[{"createdAt":"2023-11-05T17:09:34.000Z","updatedAt":"2023-11-05T17:09:34.000Z","id":"009f3a34-8164-4ff9-b981-9dcab1a518fc","channelNo":"243","programmeId":null,"title":"News Live","episode":null,"slug":"news-live","date":"2023-11-08 17:00:00","startTime":"2023-11-08 20:00:00","endTime":"2023-11-08 20:30:00","length":1800,"description":"News Live","long_description":"Up to date news and analysis from around the world.","status":true,"channel":{"id":"7fd7a9a6-af32-c861-d2b0-4ddc7846fad2","key":"AljaInt","no":243,"name":"Al Jazeera International","slug":"al-jazeera-international","website":null,"description":"<p>An international 24-hour English-language It is the first English-language news channel brings you the latest global news stories, analysis from the Middle East &amp; worldwide.</p>","shortDescription":null,"logo":"files/logos/channels/11-NEWS/AlJazeera Int SD-FirstMedia-Chl-243.jpg","externalId":"132","type":"radio","status":true,"chanel":"SD","locale":"id","relationId":"5a6ea4ae-a008-4889-9c68-7a6f1838e81d","onlyfm":null,"genress":[{"id":"1db3bb43-b00d-49af-b272-6c058a8c0b49","name":"International Free View"},{"id":"2e81a4bd-9719-4186-820a-7e035e07be13","name":"News"}]}}]}}}
+3 -2
View File
@@ -1,4 +1,6 @@
const { url, parser } = require('./firstmedia.com.config.js') const { url, parser } = require('./firstmedia.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
@@ -13,8 +15,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
'{"data":{"entries":{"243":[{"createdAt":"2023-11-05T17:09:34.000Z","updatedAt":"2023-11-05T17:09:34.000Z","id":"009f3a34-8164-4ff9-b981-9dcab1a518fc","channelNo":"243","programmeId":null,"title":"News Live","episode":null,"slug":"news-live","date":"2023-11-08 17:00:00","startTime":"2023-11-08 20:00:00","endTime":"2023-11-08 20:30:00","length":1800,"description":"News Live","long_description":"Up to date news and analysis from around the world.","status":true,"channel":{"id":"7fd7a9a6-af32-c861-d2b0-4ddc7846fad2","key":"AljaInt","no":243,"name":"Al Jazeera International","slug":"al-jazeera-international","website":null,"description":"<p>An international 24-hour English-language It is the first English-language news channel brings you the latest global news stories, analysis from the Middle East &amp; worldwide.</p>","shortDescription":null,"logo":"files/logos/channels/11-NEWS/AlJazeera Int SD-FirstMedia-Chl-243.jpg","externalId":"132","type":"radio","status":true,"chanel":"SD","locale":"id","relationId":"5a6ea4ae-a008-4889-9c68-7a6f1838e81d","onlyfm":null,"genress":[{"id":"1db3bb43-b00d-49af-b272-6c058a8c0b49","name":"International Free View"},{"id":"2e81a4bd-9719-4186-820a-7e035e07be13","name":"News"}]}}]}}}'
const results = parser({ content, channel, date }) const results = parser({ content, channel, date })
// All time in Asia/Jakarta // All time in Asia/Jakarta
@@ -0,0 +1,34 @@
{
"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
}
]
}
@@ -0,0 +1 @@
{"channel-programme":[]}
@@ -1,4 +1,6 @@
const { parser, url } = require('./foxsports.com.au.config.js') const { parser, url } = require('./foxsports.com.au.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
@@ -15,9 +17,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'{"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 => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -38,11 +38,6 @@ it('can parse response', () => {
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser( const result = parser({content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))}, channel)
{
content: '{"channel-programme":[]}'
},
channel
)
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+1
View File
@@ -0,0 +1 @@
[{"id":5856194,"publicUid":"f17b5a1d-3f2a-42a0-a11b-a08e5c9785fd","title":"בוש 4 - פרק 3","lead":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","description":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","rating":14,"ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T21:26:00Z","till":"2025-03-27T22:17:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/bw-4---prq-3,5856194","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T21:26:00Z","npvrTill":"2025-04-10T21:26:00Z","programRecordingId":7068534,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"bw-4---prq-3"},{"id":5858584,"publicUid":"1dc296c9-0c28-4202-8b20-cbef56a763f5","title":"אבא משתדל - 5. חבר","lead":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","description":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T22:17:00Z","till":"2025-03-27T22:43:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/b-mdl---5-br,5858584","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T22:17:00Z","npvrTill":"2025-04-10T22:17:00Z","programRecordingId":7071052,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"b-mdl---5-br"}]
+3 -2
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./freetv.tv.config.js') const { parser, url } = require('./freetv.tv.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -16,8 +18,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = '[{"id":5856194,"publicUid":"f17b5a1d-3f2a-42a0-a11b-a08e5c9785fd","title":"בוש 4 - פרק 3","lead":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","description":"עונה 4 חדשה לדרמה הבלשית. 3. השטן בתוך הבית: הכוח המיוחד מנסה לחקור, ומגלה אליבי שקרי עם השלכות מרעישות. הבלש סנטיאגו רוברטסון צריך לשים את קשריו האישיים בצד למען החקירה. בוש זוכה לביקור פתע לילי.כ עב","rating":14,"ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T21:26:00Z","till":"2025-03-27T22:17:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1361162/COVER/images/1361162_1736767668746.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/bw-4---prq-3,5856194","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T21:26:00Z","npvrTill":"2025-04-10T21:26:00Z","programRecordingId":7068534,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"bw-4---prq-3"},{"id":5858584,"publicUid":"1dc296c9-0c28-4202-8b20-cbef56a763f5","title":"אבא משתדל - 5. חבר","lead":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","description":"סדרה קומית. יוסי מכיר אב לילד עם צרכים מיוחדים ובין השניים מתפתח קשר בסגנון חיזור גורלי שמערער את יוסי. כ עב.","ratingEmbedded":false,"type":"PROGRAMME","uhd":false,"since":"2025-03-27T22:17:00Z","till":"2025-03-27T22:43:00Z","images":{"16x9":[{"url":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?dsth=177&dstw=315&srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1","templateUrl":"//d1zqtf09wb8nt5.cloudfront.net/scale/oil/freetv/upload/programme/1070668/COVER/images/1070668_1742202219830.jpg?srcmode=0&srcx=0&srcy=0&quality=65&type=1&srcw=1/1&srch=1/1&dsth={height:177}&dstw={width:315}"}]},"genres":[],"webUrl":"https://web.freetv.tv/news,1991/rw-11,3370462/2025-03-27/b-mdl---5-br,5858584","trailer":false,"live":{"type_":"LIVE","id":3370462},"available":true,"timeshiftAvailable":false,"startoverAvailable":false,"catchupTill":"2025-04-10T22:17:00Z","npvrTill":"2025-04-10T22:17:00Z","programRecordingId":7071052,"audio":false,"video":true,"showRecommendations":false,"recommended":false,"premiere":false,"liveBroadcast":false,"slug":"b-mdl---5-br"}]' const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content }).map(p => { const results = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -0,0 +1 @@
{"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"}]}
@@ -0,0 +1 @@
{"count":0,"next":null,"previous":null,"results":[]}
+4 -3
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./frikanalen.no.config.js') const { parser, url } = require('./frikanalen.no.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -18,8 +20,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'{"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 => { const result = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -41,7 +42,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '{"count":0,"next":null,"previous":null,"results":[]}' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+21
View File
@@ -0,0 +1,21 @@
{
"programs": [
{
"metaInfo": {
"title": "Гимн",
"description": "Государственный гимн Республики Казахстан"
},
"scheduleInfo": {
"start": 1736470800,
"end": 1736471100
},
"mediaInfo": {
"thumbnails": [
{
"url": "http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn"
}
]
}
}
]
}
@@ -0,0 +1 @@
{"programs":[]}
+4 -23
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./galamtv.kz.config.js') const { parser, url } = require('./galamtv.kz.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -21,28 +23,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = JSON.stringify({ const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
programs: [
{
metaInfo: {
title: 'Гимн',
description: 'Государственный гимн Республики Казахстан'
},
scheduleInfo: {
start: 1736470800,
end: 1736471100
},
mediaInfo: {
thumbnails: [
{
url: 'http://galam.server-img.lfstrm.tv:80/image/aHR0cDovL2dhbGFtLmltZy1vcmlnaW5hbHMubGZzdHJtLnR2OjgwL3R2aW1hZ2VzL3RodW1iL2YyNWFmYWY2ZDkzYjU5YjdkMjBiZDNiODhiZjg4NWI0X29yaWcuanBn'
}
]
}
}
]
})
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -65,7 +46,7 @@ it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,
channel, channel,
content: '{"programs":[]}' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+3 -4
View File
@@ -1,8 +1,10 @@
const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const uniqBy = require('lodash.uniqby')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@@ -41,9 +43,6 @@ module.exports = {
return programs return programs
}, },
async channels() { async channels() {
const axios = require('axios')
const _ = require('lodash')
const providers = ['-1', '-2', '-3'] const providers = ['-1', '-2', '-3']
const channels = [] const channels = []
@@ -77,7 +76,7 @@ module.exports = {
}) })
} }
return _.uniqBy(channels, 'site_id') return uniqBy(channels, 'site_id')
} }
} }
@@ -0,0 +1 @@
{"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 } ] }]}
@@ -0,0 +1 @@
{"events":[],"total":0}
+4 -3
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./guidatv.sky.it.config.js') const { parser, url } = require('./guidatv.sky.it.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -18,8 +20,7 @@ it('can generate valid url', () => {
}) })
it('can parse response', () => { it('can parse response', () => {
const content = const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
'{"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 => { const result = parser({ content }).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
@@ -45,7 +46,7 @@ it('can parse response', () => {
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
content: '{"events":[],"total":0}' content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
}) })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })
+114 -109
View File
@@ -19,11 +19,11 @@ module.exports = {
const now = dayjs() const now = dayjs()
const demain = now.add(1, 'd') const demain = now.add(1, 'd')
if (date && date.isSame(demain, 'day')) { if (date && date.isSame(demain, 'day')) {
return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}` return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}`
} else if (!date || date.isSame(now, 'day')) { } else if (!date || date.isSame(now, 'day')) {
return `https://www.guidetnt.com/tv/programme-${channel.site_id}` return `https://www.guidetnt.com/tv/programme-${channel.site_id}`
} else { } else {
return null return null
} }
}, },
async parser({ content, date }) { async parser({ content, date }) {
@@ -57,8 +57,8 @@ module.exports = {
let category = parseCategory($item) let category = parseCategory($item)
let description = parseDescription($item) let description = parseDescription($item)
const itemDetailsURL = parseDescriptionURL($item) const itemDetailsURL = parseDescriptionURL($item)
if(itemDetailsURL) { if (itemDetailsURL) {
const url = 'https://www.guidetnt.com' + itemDetailsURL const url = 'https://www.guidetnt.com' + itemDetailsURL
try { try {
const response = await axios.get(url) const response = await axios.get(url)
itemDetails = parseItemDetails(response.data) itemDetails = parseItemDetails(response.data)
@@ -66,21 +66,21 @@ module.exports = {
console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, err) console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, err)
} }
const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD')) const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD'))
start = timeRange?.start start = timeRange?.start
stop = timeRange?.stop stop = timeRange?.stop
subTitle = itemDetails?.subTitle subTitle = itemDetails?.subTitle
if (title == subTitle) subTitle = null if (title == subTitle) subTitle = null
description = itemDetails?.description description = itemDetails?.description
const categoryDetails = parseCategoryText(itemDetails?.category) const categoryDetails = parseCategoryText(itemDetails?.category)
//duration = categoryDetails?.duration //duration = categoryDetails?.duration
country = categoryDetails?.country country = categoryDetails?.country
productionDate = categoryDetails?.productionDate productionDate = categoryDetails?.productionDate
season = categoryDetails?.season season = categoryDetails?.season
episode = categoryDetails?.episode episode = categoryDetails?.episode
} }
// See https://www.npmjs.com/package/epg-parser for parameters // See https://www.npmjs.com/package/epg-parser for parameters
programs.push({ programs.push({
title, title,
@@ -110,115 +110,120 @@ module.exports = {
// Look inside each .tvlogo container // Look inside each .tvlogo container
$('.tvlogo').each((i, el) => { $('.tvlogo').each((i, el) => {
// Find all descendants that have an alt attribute // Find all descendants that have an alt attribute
$(el).find('[alt]').each((j, subEl) => { $(el)
const alt = $(subEl).attr('alt') .find('[alt]')
const href = $(subEl).attr('href') .each((j, subEl) => {
if (href && alt && alt.trim() !== '') { const alt = $(subEl).attr('alt')
const name = alt.trim() const href = $(subEl).attr('href')
const site_id = href.replace(/^\/tv\/programme-/, '') if (href && alt && alt.trim() !== '') {
channels.push({ const name = alt.trim()
lang: 'fr', const site_id = href.replace(/^\/tv\/programme-/, '')
name, channels.push({
site_id lang: 'fr',
}) name,
} site_id
}) })
}
})
}) })
return channels return channels
} }
} }
function parseTimeRange(timeRange, baseDate) { function parseTimeRange(timeRange, baseDate) {
// Split times // Split times
const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim()) const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim())
// Parse with base date // Parse with base date
const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm') const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm')
let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm') let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm')
// Handle possible day wrap (e.g., 23:30 - 00:15) // Handle possible day wrap (e.g., 23:30 - 00:15)
if (end.isBefore(start)) { if (end.isBefore(start)) {
end = end.add(1, 'day') end = end.add(1, 'day')
} }
// Calculate duration in minutes // Calculate duration in minutes
const diffMinutes = end.diff(start, 'minute') const diffMinutes = end.diff(start, 'minute')
return { return {
start: start.format(), start: start.format(),
stop: end.format(), stop: end.format(),
duration: diffMinutes duration: diffMinutes
} }
} }
function parseItemDetails(itemDetails) { function parseItemDetails(itemDetails) {
const $ = cheerio.load(itemDetails) const $ = cheerio.load(itemDetails)
const program = $('.program-wrapper').first() const program = $('.program-wrapper').first()
const programHour = program.find('.program-hour').text().trim() const programHour = program.find('.program-hour').text().trim()
const programTitle = program.find('.program-title').text().trim() const programTitle = program.find('.program-title').text().trim()
const programElementBold = program.find('.program-element-bold').text().trim() const programElementBold = program.find('.program-element-bold').text().trim()
const programArea1 = program.find('.program-element.program-area-1').text().trim() const programArea1 = program.find('.program-element.program-area-1').text().trim()
let description = '' let description = ''
const programElements = $('.program-element').filter((i, el) => { const programElements = $('.program-element').filter((i, el) => {
const classAttr = $(el).attr('class') const classAttr = $(el).attr('class')
// Return true only if it is exactly "program-element" (no extra classes) // Return true only if it is exactly "program-element" (no extra classes)
return classAttr.trim() === 'program-element' return classAttr.trim() === 'program-element'
}) })
programElements.each((i, el) => { programElements.each((i, el) => {
description += $(el).text().trim() description += $(el).text().trim()
}) })
const area2Node = $('.program-area-2').first() const area2Node = $('.program-area-2').first()
const area2 = $(area2Node) const area2 = $(area2Node)
const data = {} const data = {}
let currentLabel = null let currentLabel = null
let texts = [] let texts = []
area2.contents().each((i, node) => { area2.contents().each((i, node) => {
if (node.type === 'tag' && node.name === 'strong') { if (node.type === 'tag' && node.name === 'strong') {
// If we had collected some text for the previous label, save it // If we had collected some text for the previous label, save it
if (currentLabel && texts.length) { if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma
} }
// New label - get text without colon // New label - get text without colon
currentLabel = $(node).text().replace(/:$/, '').trim() currentLabel = $(node).text().replace(/:$/, '').trim()
texts = [] texts = []
} else if (currentLabel) { } else if (currentLabel) {
// Append the text content (text node or others) // Append the text content (text node or others)
if (node.type === 'text') { if (node.type === 'text') {
texts.push(node.data) texts.push(node.data)
} else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') { } else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') {
texts.push($(node).text()) texts.push($(node).text())
} }
} }
}) })
// Save last label text // Save last label text
if (currentLabel && texts.length) { if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '')
} }
const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null
return { return {
programHour, programHour,
title: programTitle, title: programTitle,
subTitle: programElementBold, subTitle: programElementBold,
category: programArea1, category: programArea1,
description: description, description: description,
directorActors: data, directorActors: data,
image: imgSrc image: imgSrc
} }
} }
function parseCategoryText(text) { function parseCategoryText(text) {
if (!text) return null if (!text) return null
const parts = text.split(',').map(s => s.trim()).filter(Boolean) const parts = text
.split(',')
.map(s => s.trim())
.filter(Boolean)
const len = parts.length const len = parts.length
const category = parts[0] || null const category = parts[0] || null
@@ -252,18 +257,18 @@ function parseCategoryText(text) {
//duration = [{ units: 'minutes', value: durationMinute }], //duration = [{ units: 'minutes', value: durationMinute }],
durationIndex = i durationIndex = i
} else if (parts[i].toLowerCase().includes('épisode')) { } else if (parts[i].toLowerCase().includes('épisode')) {
const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i) const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i)
if (match) { if (match) {
episode = parseInt(match[1], 10) episode = parseInt(match[1], 10)
} }
} else if (parts[i].toLowerCase().includes('saison')) { } else if (parts[i].toLowerCase().includes('saison')) {
season = parts[i].replace('saison', '').trim() season = parts[i].replace('saison', '').trim()
} }
} }
// Country: second to last // Country: second to last
const countryIndex = len - 2 const countryIndex = len - 2
let country = (durationIndex === countryIndex) ? null : parts[countryIndex] let country = durationIndex === countryIndex ? null : parts[countryIndex]
return { return {
category, category,
@@ -325,9 +330,9 @@ function parseItems(content) {
const rows = $('.channel-row').toArray() const rows = $('.channel-row').toArray()
return { return {
rows, rows,
logoSrc, logoSrc,
title, title,
formattedDate formattedDate
} }
} }
+12 -10
View File
@@ -32,7 +32,8 @@ it('can parse response', async () => {
expect(results.length).toBe(29) expect(results.length).toBe(29)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
category: 'Série', category: 'Série',
description: 'Grande effervescence pour toute l\'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d\'optimiser les révisions d\'E...', description:
"Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...",
start: '2025-06-30T22:55:00.000Z', start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z', stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis' title: 'Camping Paradis'
@@ -45,11 +46,12 @@ it('can parse response', async () => {
title: 'Programmes de la nuit' title: 'Programmes de la nuit'
}) })
expect(results[15]).toMatchObject({ expect(results[15]).toMatchObject({
category: 'Téléfilm', category: 'Téléfilm',
description: 'La vie quasi parfaite de Riley bascule brutalement lorsqu\'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l\'enquête débute, l\'affaire prend une tournure étrange l...', description:
"La vie quasi parfaite de Riley bascule brutalement lorsqu'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l'enquête débute, l'affaire prend une tournure étrange l...",
start: '2025-07-01T12:25:00.000Z', start: '2025-07-01T12:25:00.000Z',
stop: '2025-07-01T14:00:00.000Z', stop: '2025-07-01T14:00:00.000Z',
title: 'Trahie par l\'amour' title: "Trahie par l'amour"
}) })
}) })
@@ -57,16 +59,16 @@ it('can parse response for current day', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') }) let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') })
results = results.map(p => { results = results.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
} })
)
expect(results.length).toBe(29) expect(results.length).toBe(29)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
category: 'Série', category: 'Série',
description: 'Grande effervescence pour toute l\'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d\'optimiser les révisions d\'E...', description:
"Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...",
start: '2025-06-30T22:55:00.000Z', start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z', stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis' title: 'Camping Paradis'
+1
View File
@@ -0,0 +1 @@
{"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":[]}]}]}
+1
View File
@@ -0,0 +1 @@
{"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":[]}]}]}
+1
View File
@@ -0,0 +1 @@
{"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":[]}]}]}
+1
View File
@@ -0,0 +1 @@
{"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":[]}]}]}
@@ -0,0 +1 @@
{"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}
@@ -0,0 +1 @@
{"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}
@@ -0,0 +1 @@
{"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}
@@ -0,0 +1 @@
{"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}
@@ -0,0 +1 @@
[{"type":"PATH_PARAM","code":"period","reason":"INVALID"}]

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