mirror of
https://github.com/iptv-org/epg
synced 2026-03-21 19:30:52 -04:00
stricter ESLint configuration, linebreak on stylistic per deprecation by ESLint, fixed changes. add attibutes to prevent blockade.
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Enforce the usage of CRLF in GitHub Actions per ESLint configuration.
|
||||||
|
* text eol=crlf
|
||||||
@@ -1,55 +1,57 @@
|
|||||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||||
import globals from 'globals'
|
import stylistic from '@stylistic/eslint-plugin'
|
||||||
import tsParser from '@typescript-eslint/parser'
|
import globals from 'globals'
|
||||||
import path from 'node:path'
|
import tsParser from '@typescript-eslint/parser'
|
||||||
import { fileURLToPath } from 'node:url'
|
import path from 'node:path'
|
||||||
import js from '@eslint/js'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { FlatCompat } from '@eslint/eslintrc'
|
import js from '@eslint/js'
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc'
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = path.dirname(__filename)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const compat = new FlatCompat({
|
const __dirname = path.dirname(__filename)
|
||||||
baseDirectory: __dirname,
|
const compat = new FlatCompat({
|
||||||
recommendedConfig: js.configs.recommended,
|
baseDirectory: __dirname,
|
||||||
allConfig: js.configs.all
|
recommendedConfig: js.configs.recommended,
|
||||||
})
|
allConfig: js.configs.all
|
||||||
|
})
|
||||||
export default [
|
|
||||||
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'),
|
export default [
|
||||||
{
|
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/strict', 'plugin:@typescript-eslint/stylistic', 'prettier'),
|
||||||
plugins: {
|
{
|
||||||
'@typescript-eslint': typescriptEslint
|
plugins: {
|
||||||
},
|
'@typescript-eslint': typescriptEslint,
|
||||||
|
'@stylistic': stylistic
|
||||||
languageOptions: {
|
},
|
||||||
globals: {
|
|
||||||
...globals.node,
|
languageOptions: {
|
||||||
...globals.jest
|
globals: {
|
||||||
},
|
...globals.node,
|
||||||
|
...globals.jest
|
||||||
parser: tsParser,
|
},
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module'
|
parser: tsParser,
|
||||||
},
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
rules: {
|
},
|
||||||
'@typescript-eslint/no-require-imports': 'off',
|
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
rules: {
|
||||||
'no-case-declarations': 'off',
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
'linebreak-style': ['error', process.env.CI ? 'unix' : 'windows'],
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
quotes: [
|
'@stylistic/linebreak-style': ['error', 'windows'],
|
||||||
'error',
|
|
||||||
'single',
|
quotes: [
|
||||||
{
|
'error',
|
||||||
avoidEscape: true
|
'single',
|
||||||
}
|
{
|
||||||
],
|
avoidEscape: true
|
||||||
|
}
|
||||||
semi: ['error', 'never']
|
],
|
||||||
}
|
|
||||||
},
|
semi: ['error', 'never']
|
||||||
{
|
}
|
||||||
ignores: ['tests/__data__/']
|
},
|
||||||
}
|
{
|
||||||
]
|
ignores: ['tests/__data__/']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"@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",
|
||||||
|
"@stylistic/eslint-plugin": "^5.2.2",
|
||||||
"@swc/core": "^1.13.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",
|
||||||
@@ -3146,6 +3147,50 @@
|
|||||||
"@sinonjs/commons": "^3.0.1"
|
"@sinonjs/commons": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@stylistic/eslint-plugin": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-bE2DUjruqXlHYP3Q2Gpqiuj2bHq7/88FnuaS0FjeGGLCy+X6a07bGVuwtiOYnPSLHR6jmx5Bwdv+j7l8H+G97A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
|
"@typescript-eslint/types": "^8.37.0",
|
||||||
|
"eslint-visitor-keys": "^4.2.1",
|
||||||
|
"espree": "^10.4.0",
|
||||||
|
"estraverse": "^5.3.0",
|
||||||
|
"picomatch": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": ">=9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/core": {
|
"node_modules/@swc/core": {
|
||||||
"version": "1.13.2",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"@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",
|
||||||
|
"@stylistic/eslint-plugin": "^5.2.2",
|
||||||
"@swc/core": "^1.13.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",
|
||||||
|
|||||||
@@ -1,216 +1,216 @@
|
|||||||
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
|
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
|
||||||
import type { DataProcessorData } from '../../types/dataProcessor'
|
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import type { DataLoaderData } from '../../types/dataLoader'
|
import type { DataLoaderData } from '../../types/dataLoader'
|
||||||
import { ChannelSearchableData } from '../../types/channel'
|
import { ChannelSearchableData } from '../../types/channel'
|
||||||
import { Channel, ChannelList, Feed } from '../../models'
|
import { Channel, ChannelList, Feed } from '../../models'
|
||||||
import { DataProcessor, DataLoader } from '../../core'
|
import { DataProcessor, DataLoader } from '../../core'
|
||||||
import { select, input } from '@inquirer/prompts'
|
import { select, input } from '@inquirer/prompts'
|
||||||
import { ChannelsParser } from '../../core'
|
import { ChannelsParser } from '../../core'
|
||||||
import { DATA_DIR } from '../../constants'
|
import { DATA_DIR } from '../../constants'
|
||||||
import nodeCleanup from 'node-cleanup'
|
import nodeCleanup from 'node-cleanup'
|
||||||
import sjs from '@freearhey/search-js'
|
import sjs from '@freearhey/search-js'
|
||||||
import epgGrabber from 'epg-grabber'
|
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
|
||||||
.createInterface({
|
.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: process.stdout
|
||||||
})
|
})
|
||||||
.on('SIGINT', function () {
|
.on('SIGINT', function () {
|
||||||
process.emit('SIGINT')
|
process.emit('SIGINT')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
|
|
||||||
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
|
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
|
||||||
|
|
||||||
const filepath = program.args[0]
|
const filepath = program.args[0]
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
let channelList = new ChannelList({ channels: [] })
|
let channelList = new ChannelList({ channels: [] })
|
||||||
|
|
||||||
main(filepath)
|
main(filepath)
|
||||||
nodeCleanup(() => {
|
nodeCleanup(() => {
|
||||||
save(filepath, channelList)
|
save(filepath, channelList)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default async function main(filepath: string) {
|
export default async function main(filepath: string) {
|
||||||
if (!(await storage.exists(filepath))) {
|
if (!(await storage.exists(filepath))) {
|
||||||
throw new Error(`File "${filepath}" does not exists`)
|
throw new Error(`File "${filepath}" does not exists`)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
const processor = new DataProcessor()
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const data: DataLoaderData = await loader.load()
|
const data: DataLoaderData = await loader.load()
|
||||||
const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
|
const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
|
||||||
processor.process(data)
|
processor.process(data)
|
||||||
|
|
||||||
logger.info('loading channels...')
|
logger.info('loading channels...')
|
||||||
const parser = new ChannelsParser({ storage })
|
const parser = new ChannelsParser({ storage })
|
||||||
channelList = await parser.parse(filepath)
|
channelList = await parser.parse(filepath)
|
||||||
const parsedChannelsWithoutId = channelList.channels.filter(
|
const parsedChannelsWithoutId = channelList.channels.filter(
|
||||||
(channel: epgGrabber.Channel) => !channel.xmltv_id
|
(channel: epgGrabber.Channel) => !channel.xmltv_id
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
|
`found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info('creating search index...')
|
logger.info('creating search index...')
|
||||||
const items = channels.map((channel: Channel) => channel.getSearchable()).all()
|
const items = channels.map((channel: Channel) => channel.getSearchable()).all()
|
||||||
const searchIndex = sjs.createIndex(items, {
|
const searchIndex = sjs.createIndex(items, {
|
||||||
searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames']
|
searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames']
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('starting...\n')
|
logger.info('starting...\n')
|
||||||
|
|
||||||
for (const channel of parsedChannelsWithoutId.all()) {
|
for (const channel of parsedChannelsWithoutId.all()) {
|
||||||
try {
|
try {
|
||||||
channel.xmltv_id = await selectChannel(
|
channel.xmltv_id = await selectChannel(
|
||||||
channel,
|
channel,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
feedsGroupedByChannelId,
|
feedsGroupedByChannelId,
|
||||||
channelsKeyById
|
channelsKeyById
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info(err.message)
|
logger.info(err.message)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => {
|
parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => {
|
||||||
if (channel.xmltv_id === '-') {
|
if (channel.xmltv_id === '-') {
|
||||||
channel.xmltv_id = ''
|
channel.xmltv_id = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectChannel(
|
async function selectChannel(
|
||||||
channel: epgGrabber.Channel,
|
channel: epgGrabber.Channel,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
feedsGroupedByChannelId: Dictionary,
|
feedsGroupedByChannelId: Dictionary,
|
||||||
channelsKeyById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const query = escapeRegex(channel.name)
|
const query = escapeRegex(channel.name)
|
||||||
const similarChannels = searchIndex
|
const similarChannels = searchIndex
|
||||||
.search(query)
|
.search(query)
|
||||||
.map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
|
.map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
|
||||||
|
|
||||||
const selected: ChoiceValue = await select({
|
const selected: ChoiceValue = await select({
|
||||||
message: `Select channel ID for "${channel.name}" (${channel.site_id}):`,
|
message: `Select channel ID for "${channel.name}" (${channel.site_id}):`,
|
||||||
choices: getChannelChoises(new Collection(similarChannels)),
|
choices: getChannelChoises(new Collection(similarChannels)),
|
||||||
pageSize: 10
|
pageSize: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (selected.type) {
|
switch (selected.type) {
|
||||||
case 'skip':
|
case 'skip':
|
||||||
return '-'
|
return '-'
|
||||||
case 'type': {
|
case 'type': {
|
||||||
const typedChannelId = await input({ message: ' Channel ID:' })
|
const typedChannelId = await input({ message: ' Channel ID:' })
|
||||||
if (!typedChannelId) return ''
|
if (!typedChannelId) return ''
|
||||||
const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId)
|
const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId)
|
||||||
if (selectedFeedId === '-') return typedChannelId
|
if (selectedFeedId === '-') return typedChannelId
|
||||||
return [typedChannelId, selectedFeedId].join('@')
|
return [typedChannelId, selectedFeedId].join('@')
|
||||||
}
|
}
|
||||||
case 'channel': {
|
case 'channel': {
|
||||||
const selectedChannel = selected.value
|
const selectedChannel = selected.value
|
||||||
if (!selectedChannel) return ''
|
if (!selectedChannel) return ''
|
||||||
const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId)
|
const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId)
|
||||||
if (selectedFeedId === '-') return selectedChannel.id || ''
|
if (selectedFeedId === '-') return selectedChannel.id || ''
|
||||||
return [selectedChannel.id, selectedFeedId].join('@')
|
return [selectedChannel.id, selectedFeedId].join('@')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> {
|
async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> {
|
||||||
const channelFeeds = feedsGroupedByChannelId.has(channelId)
|
const channelFeeds = feedsGroupedByChannelId.has(channelId)
|
||||||
? new Collection(feedsGroupedByChannelId.get(channelId))
|
? new Collection(feedsGroupedByChannelId.get(channelId))
|
||||||
: new Collection()
|
: new Collection()
|
||||||
const choices = getFeedChoises(channelFeeds)
|
const choices = getFeedChoises(channelFeeds)
|
||||||
|
|
||||||
const selected: ChoiceValue = await select({
|
const selected: ChoiceValue = await select({
|
||||||
message: `Select feed ID for "${channelId}":`,
|
message: `Select feed ID for "${channelId}":`,
|
||||||
choices,
|
choices,
|
||||||
pageSize: 10
|
pageSize: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (selected.type) {
|
switch (selected.type) {
|
||||||
case 'skip':
|
case 'skip':
|
||||||
return '-'
|
return '-'
|
||||||
case 'type':
|
case 'type':
|
||||||
return await input({ message: ' Feed ID:', default: 'SD' })
|
return await input({ message: ' Feed ID:', default: 'SD' })
|
||||||
case 'feed':
|
case 'feed':
|
||||||
const selectedFeed = selected.value
|
const selectedFeed = selected.value
|
||||||
if (!selectedFeed) return ''
|
if (!selectedFeed) return ''
|
||||||
return selectedFeed.id || ''
|
return selectedFeed.id || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChannelChoises(channels: Collection): Choice[] {
|
function getChannelChoises(channels: Collection): Choice[] {
|
||||||
const choises: Choice[] = []
|
const choises: Choice[] = []
|
||||||
|
|
||||||
channels.forEach((channel: Channel) => {
|
channels.forEach((channel: Channel) => {
|
||||||
const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ')
|
const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ')
|
||||||
|
|
||||||
choises.push({
|
choises.push({
|
||||||
value: {
|
value: {
|
||||||
type: 'channel',
|
type: 'channel',
|
||||||
value: channel
|
value: channel
|
||||||
},
|
},
|
||||||
name: `${channel.id} (${names})`,
|
name: `${channel.id} (${names})`,
|
||||||
short: `${channel.id}`
|
short: `${channel.id}`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
choises.push({ name: 'Type...', value: { type: 'type' } })
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
|
|
||||||
return choises
|
return choises
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeedChoises(feeds: Collection): Choice[] {
|
function getFeedChoises(feeds: Collection): Choice[] {
|
||||||
const choises: Choice[] = []
|
const choises: Choice[] = []
|
||||||
|
|
||||||
feeds.forEach((feed: Feed) => {
|
feeds.forEach((feed: Feed) => {
|
||||||
let name = `${feed.id} (${feed.name})`
|
let name = `${feed.id} (${feed.name})`
|
||||||
if (feed.isMain) name += ' [main]'
|
if (feed.isMain) name += ' [main]'
|
||||||
|
|
||||||
choises.push({
|
choises.push({
|
||||||
value: {
|
value: {
|
||||||
type: 'feed',
|
type: 'feed',
|
||||||
value: feed
|
value: feed
|
||||||
},
|
},
|
||||||
default: feed.isMain,
|
default: feed.isMain,
|
||||||
name,
|
name,
|
||||||
short: feed.id
|
short: feed.id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
choises.push({ name: 'Type...', value: { type: 'type' } })
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
|
|
||||||
return choises
|
return choises
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(filepath: string, channelList: ChannelList) {
|
function save(filepath: string, channelList: ChannelList) {
|
||||||
if (!storage.existsSync(filepath)) return
|
if (!storage.existsSync(filepath)) return
|
||||||
storage.saveSync(filepath, channelList.toString())
|
storage.saveSync(filepath, channelList.toString())
|
||||||
logger.info(`\nFile '${filepath}' successfully saved`)
|
logger.info(`\nFile '${filepath}' successfully saved`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeRegex(string: string) {
|
function escapeRegex(string: string) {
|
||||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +1,86 @@
|
|||||||
import { Logger, File, Storage } from '@freearhey/core'
|
import { Logger, File, Storage } from '@freearhey/core'
|
||||||
import { ChannelsParser } from '../../core'
|
import { ChannelsParser } from '../../core'
|
||||||
import { ChannelList } from '../../models'
|
import { ChannelList } from '../../models'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
program
|
program
|
||||||
.requiredOption('-c, --config <config>', 'Config file')
|
.requiredOption('-c, --config <config>', 'Config file')
|
||||||
.option('-s, --set [args...]', 'Set custom arguments')
|
.option('-s, --set [args...]', 'Set custom arguments')
|
||||||
.option('-o, --output <output>', 'Output file')
|
.option('-o, --output <output>', 'Output file')
|
||||||
.parse(process.argv)
|
.parse(process.argv)
|
||||||
|
|
||||||
type ParseOptions = {
|
interface ParseOptions {
|
||||||
config: string
|
config: string
|
||||||
set?: string
|
set?: string
|
||||||
output?: string
|
output?: string
|
||||||
clean?: boolean
|
clean?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: ParseOptions = program.opts()
|
const options: ParseOptions = program.opts()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
function isPromise(promise: object[] | Promise<object[]>) {
|
function isPromise(promise: object[] | Promise<object[]>) {
|
||||||
return (
|
return (
|
||||||
!!promise &&
|
!!promise &&
|
||||||
typeof promise === 'object' &&
|
typeof promise === 'object' &&
|
||||||
typeof (promise as Promise<object[]>).then === 'function'
|
typeof (promise as Promise<object[]>).then === 'function'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const parser = new ChannelsParser({ storage })
|
const parser = new ChannelsParser({ storage })
|
||||||
const file = new File(options.config)
|
const file = new File(options.config)
|
||||||
const dir = file.dirname()
|
const dir = file.dirname()
|
||||||
const config = (await import(pathToFileURL(options.config).toString())).default
|
const config = (await import(pathToFileURL(options.config).toString())).default
|
||||||
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
||||||
|
|
||||||
let channelList = new ChannelList({ channels: [] })
|
let channelList = new ChannelList({ channels: [] })
|
||||||
if (await storage.exists(outputFilepath)) {
|
if (await storage.exists(outputFilepath)) {
|
||||||
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)) {
|
||||||
|
options.set.forEach((arg: string) => {
|
||||||
if (Array.isArray(options.set)) {
|
const [key, value] = arg.split(':')
|
||||||
options.set.forEach((arg: string) => {
|
args[key] = value
|
||||||
const [key, value] = arg.split(':')
|
})
|
||||||
args[key] = value
|
}
|
||||||
})
|
|
||||||
}
|
let parsedChannels = config.channels(args)
|
||||||
|
if (isPromise(parsedChannels)) {
|
||||||
let parsedChannels = config.channels(args)
|
parsedChannels = await parsedChannels
|
||||||
if (isPromise(parsedChannels)) {
|
}
|
||||||
parsedChannels = await parsedChannels
|
parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
|
||||||
}
|
channel.site = config.site
|
||||||
parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
|
|
||||||
channel.site = config.site
|
return channel
|
||||||
|
})
|
||||||
return channel
|
|
||||||
})
|
const newChannelList = new ChannelList({ channels: [] })
|
||||||
|
parsedChannels.forEach((channel: epgGrabber.Channel) => {
|
||||||
const newChannelList = new ChannelList({ channels: [] })
|
if (!channel.site_id) return
|
||||||
parsedChannels.forEach((channel: epgGrabber.Channel) => {
|
|
||||||
if (!channel.site_id) return
|
const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
|
||||||
|
|
||||||
const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
|
if (found) {
|
||||||
|
channel.xmltv_id = found.xmltv_id
|
||||||
if (found) {
|
channel.lang = found.lang
|
||||||
channel.xmltv_id = found.xmltv_id
|
}
|
||||||
channel.lang = found.lang
|
|
||||||
}
|
newChannelList.add(channel)
|
||||||
|
})
|
||||||
newChannelList.add(channel)
|
|
||||||
})
|
newChannelList.sort()
|
||||||
|
|
||||||
newChannelList.sort()
|
await storage.save(outputFilepath, newChannelList.toString())
|
||||||
|
|
||||||
await storage.save(outputFilepath, newChannelList.toString())
|
logger.info(`File '${outputFilepath}' successfully saved`)
|
||||||
|
}
|
||||||
logger.info(`File '${outputFilepath}' successfully saved`)
|
|
||||||
}
|
main()
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,92 +1,92 @@
|
|||||||
import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
|
import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
|
||||||
import { DataProcessorData } from '../../types/dataProcessor'
|
import { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import { Storage, Dictionary, File } from '@freearhey/core'
|
import { Storage, Dictionary, File } from '@freearhey/core'
|
||||||
import { DataLoaderData } from '../../types/dataLoader'
|
import { DataLoaderData } from '../../types/dataLoader'
|
||||||
import { ChannelList } from '../../models'
|
import { ChannelList } from '../../models'
|
||||||
import { DATA_DIR } from '../../constants'
|
import { DATA_DIR } from '../../constants'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { program } from 'commander'
|
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
|
||||||
xmltv_id?: string
|
xmltv_id?: string
|
||||||
site_id?: string
|
site_id?: string
|
||||||
logo?: string
|
logo?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const processor = new DataProcessor()
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const data: DataLoaderData = await loader.load()
|
const data: DataLoaderData = await loader.load()
|
||||||
const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
|
const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
|
||||||
const parser = new ChannelsParser({
|
const parser = new ChannelsParser({
|
||||||
storage: new Storage()
|
storage: new Storage()
|
||||||
})
|
})
|
||||||
|
|
||||||
let totalFiles = 0
|
let totalFiles = 0
|
||||||
let totalErrors = 0
|
let totalErrors = 0
|
||||||
|
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
||||||
for (const filepath of files) {
|
for (const filepath of files) {
|
||||||
const file = new File(filepath)
|
const file = new File(filepath)
|
||||||
if (file.extension() !== 'xml') continue
|
if (file.extension() !== 'xml') continue
|
||||||
|
|
||||||
const channelList: ChannelList = await parser.parse(filepath)
|
const channelList: ChannelList = await parser.parse(filepath)
|
||||||
|
|
||||||
const bufferBySiteId = new Dictionary()
|
const bufferBySiteId = new Dictionary()
|
||||||
const errors: ValidationError[] = []
|
const errors: ValidationError[] = []
|
||||||
channelList.channels.forEach((channel: epgGrabber.Channel) => {
|
channelList.channels.forEach((channel: epgGrabber.Channel) => {
|
||||||
const bufferId: string = channel.site_id
|
const bufferId: string = channel.site_id
|
||||||
if (bufferBySiteId.missing(bufferId)) {
|
if (bufferBySiteId.missing(bufferId)) {
|
||||||
bufferBySiteId.set(bufferId, true)
|
bufferBySiteId.set(bufferId, true)
|
||||||
} else {
|
} else {
|
||||||
errors.push({ type: 'duplicate', ...channel })
|
errors.push({ type: 'duplicate', ...channel })
|
||||||
totalErrors++
|
totalErrors++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!langs.where('1', channel.lang ?? '')) {
|
if (!langs.where('1', channel.lang ?? '')) {
|
||||||
errors.push({ type: 'wrong_lang', ...channel })
|
errors.push({ type: 'wrong_lang', ...channel })
|
||||||
totalErrors++
|
totalErrors++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!channel.xmltv_id) return
|
if (!channel.xmltv_id) return
|
||||||
const [channelId, feedId] = channel.xmltv_id.split('@')
|
const [channelId, feedId] = channel.xmltv_id.split('@')
|
||||||
|
|
||||||
const foundChannel = channelsKeyById.get(channelId)
|
const foundChannel = channelsKeyById.get(channelId)
|
||||||
if (!foundChannel) {
|
if (!foundChannel) {
|
||||||
errors.push({ type: 'wrong_channel_id', ...channel })
|
errors.push({ type: 'wrong_channel_id', ...channel })
|
||||||
totalErrors++
|
totalErrors++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feedId) {
|
if (feedId) {
|
||||||
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
|
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||||
if (!foundFeed) {
|
if (!foundFeed) {
|
||||||
errors.push({ type: 'wrong_feed_id', ...channel })
|
errors.push({ type: 'wrong_feed_id', ...channel })
|
||||||
totalErrors++
|
totalErrors++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
console.log(chalk.underline(filepath))
|
console.log(chalk.underline(filepath))
|
||||||
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
|
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
|
||||||
console.log()
|
console.log()
|
||||||
totalFiles++
|
totalFiles++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalErrors > 0) {
|
if (totalErrors > 0) {
|
||||||
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
|
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,133 +1,133 @@
|
|||||||
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
|
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
|
||||||
import { QueueCreator, Job, ChannelsParser } from '../../core'
|
import { QueueCreator, Job, ChannelsParser } from '../../core'
|
||||||
import { Option, program } from 'commander'
|
import { Option, program } from 'commander'
|
||||||
import { SITES_DIR } from '../../constants'
|
import { SITES_DIR } from '../../constants'
|
||||||
import { Channel } from 'epg-grabber'
|
import { Channel } from 'epg-grabber'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { ChannelList } from '../../models'
|
import { ChannelList } from '../../models'
|
||||||
|
|
||||||
program
|
program
|
||||||
.addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
|
.addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option(
|
new Option(
|
||||||
'-c, --channels <path>',
|
'-c, --channels <path>',
|
||||||
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
|
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml'))
|
.addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml'))
|
||||||
.addOption(new Option('-l, --lang <codes>', 'Filter channels by languages (ISO 639-1 codes)'))
|
.addOption(new Option('-l, --lang <codes>', 'Filter channels by languages (ISO 639-1 codes)'))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env(
|
new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env(
|
||||||
'TIMEOUT'
|
'TIMEOUT'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('-d, --delay <milliseconds>', 'Override the default delay between request').env(
|
new Option('-d, --delay <milliseconds>', 'Override the default delay between request').env(
|
||||||
'DELAY'
|
'DELAY'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOption(new Option('-x, --proxy <url>', 'Use the specified proxy').env('PROXY'))
|
.addOption(new Option('-x, --proxy <url>', 'Use the specified proxy').env('PROXY'))
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option(
|
new Option(
|
||||||
'--days <days>',
|
'--days <days>',
|
||||||
'Override the number of days for which the program will be loaded (defaults to the value from the site config)'
|
'Override the number of days for which the program will be loaded (defaults to the value from the site config)'
|
||||||
)
|
)
|
||||||
.argParser(value => parseInt(value))
|
.argParser(value => parseInt(value))
|
||||||
.env('DAYS')
|
.env('DAYS')
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('--maxConnections <number>', 'Limit on the number of concurrent requests')
|
new Option('--maxConnections <number>', 'Limit on the number of concurrent requests')
|
||||||
.default(1)
|
.default(1)
|
||||||
.env('MAX_CONNECTIONS')
|
.env('MAX_CONNECTIONS')
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('--gzip', 'Create a compressed version of the guide as well')
|
new Option('--gzip', 'Create a compressed version of the guide as well')
|
||||||
.default(false)
|
.default(false)
|
||||||
.env('GZIP')
|
.env('GZIP')
|
||||||
)
|
)
|
||||||
.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
|
||||||
gzip: boolean
|
gzip: boolean
|
||||||
curl: boolean
|
curl: boolean
|
||||||
maxConnections: number
|
maxConnections: number
|
||||||
timeout?: string
|
timeout?: string
|
||||||
delay?: string
|
delay?: string
|
||||||
lang?: string
|
lang?: string
|
||||||
days?: number
|
days?: number
|
||||||
proxy?: string
|
proxy?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: GrabOptions = program.opts()
|
const options: GrabOptions = program.opts()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (!options.site && !options.channels)
|
if (!options.site && !options.channels)
|
||||||
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
|
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
|
||||||
|
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.start('starting...')
|
logger.start('starting...')
|
||||||
|
|
||||||
logger.info('config:')
|
logger.info('config:')
|
||||||
logger.tree(options)
|
logger.tree(options)
|
||||||
|
|
||||||
logger.info('loading channels...')
|
logger.info('loading channels...')
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
const parser = new ChannelsParser({ storage })
|
const parser = new ChannelsParser({ storage })
|
||||||
|
|
||||||
let files: string[] = []
|
let files: string[] = []
|
||||||
if (options.site) {
|
if (options.site) {
|
||||||
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
|
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
|
||||||
pattern = pattern.replace(/\\/g, '/')
|
pattern = pattern.replace(/\\/g, '/')
|
||||||
files = await storage.list(pattern)
|
files = await storage.list(pattern)
|
||||||
} else if (options.channels) {
|
} else if (options.channels) {
|
||||||
files = await storage.list(options.channels)
|
files = await storage.list(options.channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
let channels = new Collection()
|
let channels = new Collection()
|
||||||
for (const filepath of files) {
|
for (const filepath of files) {
|
||||||
const channelList: ChannelList = await parser.parse(filepath)
|
const channelList: ChannelList = await parser.parse(filepath)
|
||||||
|
|
||||||
channels = channels.concat(channelList.channels)
|
channels = channels.concat(channelList.channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.lang) {
|
if (options.lang) {
|
||||||
channels = channels.filter((channel: Channel) => {
|
channels = channels.filter((channel: Channel) => {
|
||||||
if (!options.lang || !channel.lang) return true
|
if (!options.lang || !channel.lang) return true
|
||||||
|
|
||||||
return options.lang.includes(channel.lang)
|
return options.lang.includes(channel.lang)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(` found ${channels.count()} channel(s)`)
|
logger.info(` found ${channels.count()} channel(s)`)
|
||||||
|
|
||||||
logger.info('run:')
|
logger.info('run:')
|
||||||
runJob({ logger, channels })
|
runJob({ logger, channels })
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
|
async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
|
||||||
const timer = new Timer()
|
const timer = new Timer()
|
||||||
timer.start()
|
timer.start()
|
||||||
|
|
||||||
const queueCreator = new QueueCreator({
|
const queueCreator = new QueueCreator({
|
||||||
channels,
|
channels,
|
||||||
logger,
|
logger,
|
||||||
options
|
options
|
||||||
})
|
})
|
||||||
const queue = await queueCreator.create()
|
const queue = await queueCreator.create()
|
||||||
const job = new Job({
|
const job = new Job({
|
||||||
queue,
|
queue,
|
||||||
logger,
|
logger,
|
||||||
options
|
options
|
||||||
})
|
})
|
||||||
|
|
||||||
await job.run()
|
await job.run()
|
||||||
|
|
||||||
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { parseChannels } from 'epg-grabber'
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChannelsParser {
|
export class ChannelsParser {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
|
|
||||||
constructor({ storage }: ChannelsParserProps) {
|
constructor({ storage }: ChannelsParserProps) {
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(filepath: string): Promise<ChannelList> {
|
async parse(filepath: string): Promise<ChannelList> {
|
||||||
const content = await this.storage.load(filepath)
|
const content = await this.storage.load(filepath)
|
||||||
const parsed = parseChannels(content)
|
const parsed = parseChannels(content)
|
||||||
|
|
||||||
return new ChannelList({ channels: parsed })
|
return new ChannelList({ channels: parsed })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,55 @@
|
|||||||
import { Channel, Feed, GuideChannel, Logo, Stream } from '../models'
|
import { Channel, Feed, GuideChannel, Logo, Stream } from '../models'
|
||||||
import { DataLoaderData } from '../types/dataLoader'
|
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))
|
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||||
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
|
||||||
|
const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
|
||||||
const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
|
const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
|
||||||
const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
|
channel.getStreamId()
|
||||||
channel.getStreamId()
|
)
|
||||||
)
|
|
||||||
|
const streams = new Collection(data.streams).map(data => new Stream(data))
|
||||||
const streams = new Collection(data.streams).map(data => new Stream(data))
|
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||||
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
|
||||||
|
let feeds = new Collection(data.feeds).map(data =>
|
||||||
let feeds = new Collection(data.feeds).map(data =>
|
new Feed(data)
|
||||||
new Feed(data)
|
.withGuideChannels(guideChannelsGroupedByStreamId)
|
||||||
.withGuideChannels(guideChannelsGroupedByStreamId)
|
.withStreams(streamsGroupedById)
|
||||||
.withStreams(streamsGroupedById)
|
.withChannel(channelsKeyById)
|
||||||
.withChannel(channelsKeyById)
|
)
|
||||||
)
|
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId())
|
||||||
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId())
|
|
||||||
|
const logos = new Collection(data.logos).map(data =>
|
||||||
const logos = new Collection(data.logos).map(data =>
|
new Logo(data).withFeed(feedsKeyByStreamId)
|
||||||
new Logo(data).withFeed(feedsKeyByStreamId)
|
)
|
||||||
)
|
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
|
||||||
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
|
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
|
||||||
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
|
|
||||||
|
feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId))
|
||||||
feeds = feeds.map((feed: Feed) => feed.withLogos(logosGroupedByStreamId))
|
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
||||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
|
||||||
|
channels = channels.map((channel: Channel) =>
|
||||||
channels = channels.map((channel: Channel) =>
|
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
|
||||||
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
|
)
|
||||||
)
|
|
||||||
|
return {
|
||||||
return {
|
guideChannelsGroupedByStreamId,
|
||||||
guideChannelsGroupedByStreamId,
|
feedsGroupedByChannelId,
|
||||||
feedsGroupedByChannelId,
|
logosGroupedByChannelId,
|
||||||
logosGroupedByChannelId,
|
logosGroupedByStreamId,
|
||||||
logosGroupedByStreamId,
|
streamsGroupedById,
|
||||||
streamsGroupedById,
|
feedsKeyByStreamId,
|
||||||
feedsKeyByStreamId,
|
channelsKeyById,
|
||||||
channelsKeyById,
|
guideChannels,
|
||||||
guideChannels,
|
channels,
|
||||||
channels,
|
streams,
|
||||||
streams,
|
feeds,
|
||||||
feeds,
|
logos
|
||||||
logos
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,105 +1,105 @@
|
|||||||
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
|
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
|
||||||
import { Logger, Collection } from '@freearhey/core'
|
import { Logger, Collection } from '@freearhey/core'
|
||||||
import { Queue, ProxyParser } from './'
|
import { Queue, ProxyParser } from './'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Grabber {
|
export class Grabber {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
queue: Queue
|
queue: Queue
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
grabber: EPGGrabber | EPGGrabberMock
|
grabber: EPGGrabber | EPGGrabberMock
|
||||||
|
|
||||||
constructor({ logger, queue, options }: GrabberProps) {
|
constructor({ logger, queue, options }: GrabberProps) {
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.queue = queue
|
this.queue = queue
|
||||||
this.options = options
|
this.options = options
|
||||||
this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber()
|
this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber()
|
||||||
}
|
}
|
||||||
|
|
||||||
async grab(): Promise<{ channels: Collection; programs: Collection }> {
|
async grab(): Promise<{ channels: Collection; programs: Collection }> {
|
||||||
const proxyParser = new ProxyParser()
|
const proxyParser = new ProxyParser()
|
||||||
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
|
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
|
||||||
|
|
||||||
const total = this.queue.size()
|
const total = this.queue.size()
|
||||||
|
|
||||||
const channels = new Collection()
|
const channels = new Collection()
|
||||||
let programs = new Collection()
|
let programs = new Collection()
|
||||||
let i = 1
|
let i = 1
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.queue.items().map(
|
this.queue.items().map(
|
||||||
taskQueue.wrap(
|
taskQueue.wrap(
|
||||||
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
|
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
|
||||||
const { channel, config, date } = queueItem
|
const { channel, config, date } = queueItem
|
||||||
|
|
||||||
channels.add(channel)
|
channels.add(channel)
|
||||||
|
|
||||||
if (this.options.timeout !== undefined) {
|
if (this.options.timeout !== undefined) {
|
||||||
const timeout = parseInt(this.options.timeout)
|
const timeout = parseInt(this.options.timeout)
|
||||||
config.request = { ...config.request, ...{ timeout } }
|
config.request = { ...config.request, ...{ timeout } }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.delay !== undefined) {
|
if (this.options.delay !== undefined) {
|
||||||
const delay = parseInt(this.options.delay)
|
const delay = parseInt(this.options.delay)
|
||||||
config.delay = delay
|
config.delay = delay
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.proxy !== undefined) {
|
if (this.options.proxy !== undefined) {
|
||||||
const proxy = proxyParser.parse(this.options.proxy)
|
const proxy = proxyParser.parse(this.options.proxy)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
proxy.protocol &&
|
proxy.protocol &&
|
||||||
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
||||||
) {
|
) {
|
||||||
const socksProxyAgent = new SocksProxyAgent(this.options.proxy)
|
const socksProxyAgent = new SocksProxyAgent(this.options.proxy)
|
||||||
|
|
||||||
config.request = {
|
config.request = {
|
||||||
...config.request,
|
...config.request,
|
||||||
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
config.request = { ...config.request, ...{ proxy } }
|
config.request = { ...config.request, ...{ proxy } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.curl === true) {
|
if (this.options.curl === true) {
|
||||||
config.curl = true
|
config.curl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const _programs = await this.grabber.grab(
|
const _programs = await this.grabber.grab(
|
||||||
channel,
|
channel,
|
||||||
date,
|
date,
|
||||||
config,
|
config,
|
||||||
(data: GrabCallbackData, error: Error | null) => {
|
(data: GrabCallbackData, error: Error | null) => {
|
||||||
const { programs, date } = data
|
const { programs, date } = data
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
|
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
|
||||||
channel.xmltv_id
|
channel.xmltv_id
|
||||||
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
|
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
|
||||||
)
|
)
|
||||||
if (i < total) i++
|
if (i < total) i++
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.info(` ERR: ${error.message}`)
|
this.logger.info(` ERR: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
programs = programs.concat(new Collection(_programs))
|
programs = programs.concat(new Collection(_programs))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return { channels, programs }
|
return { channels, programs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +1,111 @@
|
|||||||
import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
|
import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { OptionValues } from 'commander'
|
import { OptionValues } from 'commander'
|
||||||
import { Channel, Feed, Guide } from '../models'
|
import { Channel, Feed, Guide } from '../models'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { DataLoader, DataProcessor } from '.'
|
import { DataLoader, DataProcessor } from '.'
|
||||||
import { DataLoaderData } from '../types/dataLoader'
|
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
|
||||||
programs: Collection
|
programs: Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GuideManager {
|
export class GuideManager {
|
||||||
options: OptionValues
|
options: OptionValues
|
||||||
logger: Logger
|
logger: Logger
|
||||||
channels: Collection
|
channels: Collection
|
||||||
programs: Collection
|
programs: Collection
|
||||||
|
|
||||||
constructor({ channels, programs, logger, options }: GuideManagerProps) {
|
constructor({ channels, programs, logger, options }: GuideManagerProps) {
|
||||||
this.options = options
|
this.options = options
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.channels = channels
|
this.channels = channels
|
||||||
this.programs = programs
|
this.programs = programs
|
||||||
}
|
}
|
||||||
|
|
||||||
async createGuides() {
|
async createGuides() {
|
||||||
const pathTemplate = new StringTemplate(this.options.output)
|
const pathTemplate = new StringTemplate(this.options.output)
|
||||||
|
|
||||||
const processor = new DataProcessor()
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const loader = new DataLoader({ storage: dataStorage })
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const data: DataLoaderData = await loader.load()
|
const data: DataLoaderData = await loader.load()
|
||||||
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
|
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
|
||||||
|
|
||||||
const groupedChannels = this.channels
|
const groupedChannels = this.channels
|
||||||
.map((channel: epgGrabber.Channel) => {
|
.map((channel: epgGrabber.Channel) => {
|
||||||
if (channel.xmltv_id && !channel.icon) {
|
if (channel.xmltv_id && !channel.icon) {
|
||||||
const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
|
const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||||
if (foundFeed && foundFeed.hasLogo()) {
|
if (foundFeed && foundFeed.hasLogo()) {
|
||||||
channel.icon = foundFeed.getLogoUrl()
|
channel.icon = foundFeed.getLogoUrl()
|
||||||
} else {
|
} else {
|
||||||
const [channelId] = channel.xmltv_id.split('@')
|
const [channelId] = channel.xmltv_id.split('@')
|
||||||
const foundChannel: Channel = channelsKeyById.get(channelId)
|
const foundChannel: Channel = channelsKeyById.get(channelId)
|
||||||
if (foundChannel && foundChannel.hasLogo()) {
|
if (foundChannel && foundChannel.hasLogo()) {
|
||||||
channel.icon = foundChannel.getLogoUrl()
|
channel.icon = foundChannel.getLogoUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
})
|
})
|
||||||
.orderBy([
|
.orderBy([
|
||||||
(channel: epgGrabber.Channel) => channel.index,
|
(channel: epgGrabber.Channel) => channel.index,
|
||||||
(channel: epgGrabber.Channel) => channel.xmltv_id
|
(channel: epgGrabber.Channel) => channel.xmltv_id
|
||||||
])
|
])
|
||||||
.uniqBy(
|
.uniqBy(
|
||||||
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
|
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
|
||||||
)
|
)
|
||||||
.groupBy((channel: epgGrabber.Channel) => {
|
.groupBy((channel: epgGrabber.Channel) => {
|
||||||
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
|
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupedPrograms = this.programs
|
const groupedPrograms = this.programs
|
||||||
.orderBy([
|
.orderBy([
|
||||||
(program: epgGrabber.Program) => program.channel,
|
(program: epgGrabber.Program) => program.channel,
|
||||||
(program: epgGrabber.Program) => program.start
|
(program: epgGrabber.Program) => program.start
|
||||||
])
|
])
|
||||||
.groupBy((program: epgGrabber.Program) => {
|
.groupBy((program: epgGrabber.Program) => {
|
||||||
const lang =
|
const lang =
|
||||||
program.titles && program.titles.length && program.titles[0].lang
|
program.titles && program.titles.length && program.titles[0].lang
|
||||||
? program.titles[0].lang
|
? program.titles[0].lang
|
||||||
: 'en'
|
: 'en'
|
||||||
|
|
||||||
return pathTemplate.format({ lang, site: program.site || '' })
|
return pathTemplate.format({ lang, site: program.site || '' })
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const groupKey of groupedPrograms.keys()) {
|
for (const groupKey of groupedPrograms.keys()) {
|
||||||
const guide = new Guide({
|
const guide = new Guide({
|
||||||
filepath: groupKey,
|
filepath: groupKey,
|
||||||
gzip: this.options.gzip,
|
gzip: this.options.gzip,
|
||||||
channels: new Collection(groupedChannels.get(groupKey)),
|
channels: new Collection(groupedChannels.get(groupKey)),
|
||||||
programs: new Collection(groupedPrograms.get(groupKey))
|
programs: new Collection(groupedPrograms.get(groupKey))
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.save(guide)
|
await this.save(guide)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(guide: Guide) {
|
async save(guide: Guide) {
|
||||||
const storage = new Storage(path.dirname(guide.filepath))
|
const storage = new Storage(path.dirname(guide.filepath))
|
||||||
const xmlFilepath = guide.filepath
|
const xmlFilepath = guide.filepath
|
||||||
const xmlFilename = path.basename(xmlFilepath)
|
const xmlFilename = path.basename(xmlFilepath)
|
||||||
this.logger.info(` saving to "${xmlFilepath}"...`)
|
this.logger.info(` saving to "${xmlFilepath}"...`)
|
||||||
const xmltv = guide.toString()
|
const xmltv = guide.toString()
|
||||||
await storage.save(xmlFilename, xmltv)
|
await storage.save(xmlFilename, xmltv)
|
||||||
|
|
||||||
if (guide.gzip) {
|
if (guide.gzip) {
|
||||||
const zip = new Zip()
|
const zip = new Zip()
|
||||||
const compressed = zip.compress(xmltv)
|
const compressed = zip.compress(xmltv)
|
||||||
const gzFilepath = `${guide.filepath}.gz`
|
const gzFilepath = `${guide.filepath}.gz`
|
||||||
const gzFilename = path.basename(gzFilepath)
|
const gzFilename = path.basename(gzFilepath)
|
||||||
this.logger.info(` saving to "${gzFilepath}"...`)
|
this.logger.info(` saving to "${gzFilepath}"...`)
|
||||||
await storage.save(gzFilename, compressed)
|
await storage.save(gzFilename, compressed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
type Column = {
|
interface Column {
|
||||||
name: string
|
name: string
|
||||||
nowrap?: boolean
|
nowrap?: boolean
|
||||||
align?: string
|
align?: string
|
||||||
colspan?: number
|
colspan?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataItem = {
|
type DataItem = {
|
||||||
value: string
|
value: string
|
||||||
nowrap?: boolean
|
nowrap?: boolean
|
||||||
align?: string
|
align?: string
|
||||||
colspan?: number
|
colspan?: number
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
export class HTMLTable {
|
export class HTMLTable {
|
||||||
data: DataItem[]
|
data: DataItem[]
|
||||||
columns: Column[]
|
columns: Column[]
|
||||||
|
|
||||||
constructor(data: DataItem[], columns: Column[]) {
|
constructor(data: DataItem[], columns: Column[]) {
|
||||||
this.data = data
|
this.data = data
|
||||||
this.columns = columns
|
this.columns = columns
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
let output = '<table>\r\n'
|
let output = '<table>\r\n'
|
||||||
|
|
||||||
output += ' <thead>\r\n <tr>'
|
output += ' <thead>\r\n <tr>'
|
||||||
for (const column of this.columns) {
|
for (const column of this.columns) {
|
||||||
const nowrap = column.nowrap ? ' nowrap' : ''
|
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||||
const align = column.align ? ` align="${column.align}"` : ''
|
const align = column.align ? ` align="${column.align}"` : ''
|
||||||
const colspan = column.colspan ? ` colspan="${column.colspan}"` : ''
|
const colspan = column.colspan ? ` colspan="${column.colspan}"` : ''
|
||||||
|
|
||||||
output += `<th${align}${nowrap}${colspan}>${column.name}</th>`
|
output += `<th${align}${nowrap}${colspan}>${column.name}</th>`
|
||||||
}
|
}
|
||||||
output += '</tr>\r\n </thead>\r\n'
|
output += '</tr>\r\n </thead>\r\n'
|
||||||
|
|
||||||
output += ' <tbody>\r\n'
|
output += ' <tbody>\r\n'
|
||||||
for (const row of this.data) {
|
for (const row of this.data) {
|
||||||
output += ' <tr>'
|
output += ' <tr>'
|
||||||
for (const item of row) {
|
for (const item of row) {
|
||||||
const nowrap = item.nowrap ? ' nowrap' : ''
|
const nowrap = item.nowrap ? ' nowrap' : ''
|
||||||
const align = item.align ? ` align="${item.align}"` : ''
|
const align = item.align ? ` align="${item.align}"` : ''
|
||||||
const colspan = item.colspan ? ` colspan="${item.colspan}"` : ''
|
const colspan = item.colspan ? ` colspan="${item.colspan}"` : ''
|
||||||
|
|
||||||
output += `<td${align}${nowrap}${colspan}>${item.value}</td>`
|
output += `<td${align}${nowrap}${colspan}>${item.value}</td>`
|
||||||
}
|
}
|
||||||
output += '</tr>\r\n'
|
output += '</tr>\r\n'
|
||||||
}
|
}
|
||||||
output += ' </tbody>\r\n'
|
output += ' </tbody>\r\n'
|
||||||
|
|
||||||
output += '</table>'
|
output += '</table>'
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import { Logger } from '@freearhey/core'
|
import { Logger } from '@freearhey/core'
|
||||||
import { Queue, Grabber, GuideManager } from '.'
|
import { Queue, Grabber, GuideManager } from '.'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
|
|
||||||
type JobProps = {
|
interface JobProps {
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
logger: Logger
|
logger: Logger
|
||||||
queue: Queue
|
queue: Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Job {
|
export class Job {
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
logger: Logger
|
logger: Logger
|
||||||
grabber: Grabber
|
grabber: Grabber
|
||||||
|
|
||||||
constructor({ queue, logger, options }: JobProps) {
|
constructor({ queue, logger, options }: JobProps) {
|
||||||
this.options = options
|
this.options = options
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.grabber = new Grabber({ logger, queue, options })
|
this.grabber = new Grabber({ logger, queue, options })
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const { channels, programs } = await this.grabber.grab()
|
const { channels, programs } = await this.grabber.grab()
|
||||||
|
|
||||||
const manager = new GuideManager({
|
const manager = new GuideManager({
|
||||||
channels,
|
channels,
|
||||||
programs,
|
programs,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
logger: this.logger
|
logger: this.logger
|
||||||
})
|
})
|
||||||
|
|
||||||
await manager.createGuides()
|
await manager.createGuides()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
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
|
||||||
password?: string
|
password?: string
|
||||||
}
|
}
|
||||||
host: string
|
host: string
|
||||||
port: number | null
|
port: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProxyParser {
|
export class ProxyParser {
|
||||||
parse(_url: string): ProxyParserResult {
|
parse(_url: string): ProxyParserResult {
|
||||||
const parsed = new URL(_url)
|
const parsed = new URL(_url)
|
||||||
|
|
||||||
const result: ProxyParserResult = {
|
const result: ProxyParserResult = {
|
||||||
protocol: parsed.protocol.replace(':', '') || null,
|
protocol: parsed.protocol.replace(':', '') || null,
|
||||||
host: parsed.hostname,
|
host: parsed.hostname,
|
||||||
port: parsed.port ? parseInt(parsed.port) : null
|
port: parsed.port ? parseInt(parsed.port) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.username || parsed.password) {
|
if (parsed.username || parsed.password) {
|
||||||
result.auth = {}
|
result.auth = {}
|
||||||
if (parsed.username) result.auth.username = parsed.username
|
if (parsed.username) result.auth.username = parsed.username
|
||||||
if (parsed.password) result.auth.password = parsed.password
|
if (parsed.password) result.auth.password = parsed.password
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import { SiteConfig, Channel } from 'epg-grabber'
|
import { SiteConfig, Channel } from 'epg-grabber'
|
||||||
|
|
||||||
export type QueueItem = {
|
export interface QueueItem {
|
||||||
channel: Channel
|
channel: Channel
|
||||||
date: string
|
date: string
|
||||||
config: SiteConfig
|
config: SiteConfig
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Queue {
|
export class Queue {
|
||||||
_data: Dictionary
|
_data: Dictionary
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = new Dictionary()
|
this._data = new Dictionary()
|
||||||
}
|
}
|
||||||
|
|
||||||
missing(key: string): boolean {
|
missing(key: string): boolean {
|
||||||
return this._data.missing(key)
|
return this._data.missing(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
add(
|
add(
|
||||||
key: string,
|
key: string,
|
||||||
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
|
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
|
||||||
) {
|
) {
|
||||||
this._data.set(key, {
|
this._data.set(key, {
|
||||||
channel,
|
channel,
|
||||||
date,
|
date,
|
||||||
config,
|
config,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
size(): number {
|
size(): number {
|
||||||
return Object.values(this._data.data()).length
|
return Object.values(this._data.data()).length
|
||||||
}
|
}
|
||||||
|
|
||||||
items(): QueueItem[] {
|
items(): QueueItem[] {
|
||||||
return Object.values(this._data.data()) as QueueItem[]
|
return Object.values(this._data.data()) as QueueItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this.size() === 0
|
return this.size() === 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
|
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
|
||||||
import { SITES_DIR, DATA_DIR } from '../constants'
|
import { SITES_DIR, DATA_DIR } from '../constants'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
import { ConfigLoader, Queue } from './'
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueueCreator {
|
export class QueueCreator {
|
||||||
configLoader: ConfigLoader
|
configLoader: ConfigLoader
|
||||||
logger: Logger
|
logger: Logger
|
||||||
sitesStorage: Storage
|
sitesStorage: Storage
|
||||||
dataStorage: Storage
|
dataStorage: Storage
|
||||||
channels: Collection
|
channels: Collection
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
|
|
||||||
constructor({ channels, logger, options }: QueueCreatorProps) {
|
constructor({ channels, logger, options }: QueueCreatorProps) {
|
||||||
this.channels = channels
|
this.channels = channels
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.sitesStorage = new Storage()
|
this.sitesStorage = new Storage()
|
||||||
this.dataStorage = new Storage(DATA_DIR)
|
this.dataStorage = new Storage(DATA_DIR)
|
||||||
this.options = options
|
this.options = options
|
||||||
this.configLoader = new ConfigLoader()
|
this.configLoader = new ConfigLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(): Promise<Queue> {
|
async create(): Promise<Queue> {
|
||||||
let index = 0
|
let index = 0
|
||||||
const queue = new Queue()
|
const queue = new Queue()
|
||||||
for (const channel of this.channels.all()) {
|
for (const channel of this.channels.all()) {
|
||||||
channel.index = index++
|
channel.index = index++
|
||||||
if (!channel.site || !channel.site_id || !channel.name) continue
|
if (!channel.site || !channel.site_id || !channel.name) continue
|
||||||
|
|
||||||
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
|
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
|
||||||
const config: SiteConfig = await this.configLoader.load(configPath)
|
const config: SiteConfig = await this.configLoader.load(configPath)
|
||||||
|
|
||||||
if (!channel.xmltv_id) {
|
if (!channel.xmltv_id) {
|
||||||
channel.xmltv_id = channel.site_id
|
channel.xmltv_id = channel.site_id
|
||||||
}
|
}
|
||||||
|
|
||||||
const days = this.options.days || config.days || 1
|
const days = this.options.days || config.days || 1
|
||||||
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString())
|
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString())
|
||||||
const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd'))
|
const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd'))
|
||||||
dates.forEach((date: DateTime) => {
|
dates.forEach((date: DateTime) => {
|
||||||
const dateString = date.toJSON()
|
const dateString = date.toJSON()
|
||||||
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
|
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
|
||||||
if (queue.missing(key)) {
|
if (queue.missing(key)) {
|
||||||
queue.add(key, {
|
queue.add(key, {
|
||||||
channel,
|
channel,
|
||||||
date: dateString,
|
date: dateString,
|
||||||
config
|
config
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return queue
|
return queue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
/**
|
/**
|
||||||
* Sorts an array by the result of running each element through an iteratee function.
|
* Sorts an array by the result of running each element through an iteratee function.
|
||||||
* Creates a shallow copy of the array before sorting to avoid mutating the original.
|
* Creates a shallow copy of the array before sorting to avoid mutating the original.
|
||||||
*
|
*
|
||||||
* @param {Array} arr - The array to sort
|
* @param {Array} arr - The array to sort
|
||||||
* @param {Function} fn - The iteratee function to compute sort values
|
* @param {Function} fn - The iteratee function to compute sort values
|
||||||
* @returns {Array} A new sorted array
|
* @returns {Array} A new sorted array
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}];
|
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}];
|
||||||
* sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}]
|
* sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}]
|
||||||
*/
|
*/
|
||||||
export const sortBy = <T>(arr: T[], fn: (item: T) => number | string): T[] =>
|
export const sortBy = <T>(arr: T[], fn: (item: T) => number | string): T[] =>
|
||||||
[...arr].sort((a, b) => (fn(a) > fn(b) ? 1 : -1))
|
[...arr].sort((a, b) => (fn(a) > fn(b) ? 1 : -1))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts an array by multiple criteria with customizable sort orders.
|
* Sorts an array by multiple criteria with customizable sort orders.
|
||||||
* Supports ascending (default) and descending order for each criterion.
|
* Supports ascending (default) and descending order for each criterion.
|
||||||
*
|
*
|
||||||
* @param {Array} arr - The array to sort
|
* @param {Array} arr - The array to sort
|
||||||
* @param {Array<Function>} fns - Array of iteratee functions to compute sort values
|
* @param {Array<Function>} fns - Array of iteratee functions to compute sort values
|
||||||
* @param {Array<string>} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc'
|
* @param {Array<string>} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc'
|
||||||
* @returns {Array} A new sorted array
|
* @returns {Array} A new sorted array
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}];
|
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}];
|
||||||
* orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']);
|
* orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']);
|
||||||
* // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}]
|
* // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}]
|
||||||
*/
|
*/
|
||||||
export const orderBy = (
|
export const orderBy = (
|
||||||
arr: Array<unknown>,
|
arr: unknown[],
|
||||||
fns: Array<(item: unknown) => string | number>,
|
fns: ((item: unknown) => string | number)[],
|
||||||
orders: Array<string> = []
|
orders: string[] = []
|
||||||
): Array<unknown> =>
|
): unknown[] =>
|
||||||
[...arr].sort((a, b) =>
|
[...arr].sort((a, b) =>
|
||||||
fns.reduce(
|
fns.reduce(
|
||||||
(acc, fn, i) =>
|
(acc, fn, i) =>
|
||||||
acc ||
|
acc ||
|
||||||
((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1),
|
((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a duplicate-free version of an array using an iteratee function to generate
|
* Creates a duplicate-free version of an array using an iteratee function to generate
|
||||||
* the criterion by which uniqueness is computed. Only the first occurrence of each
|
* the criterion by which uniqueness is computed. Only the first occurrence of each
|
||||||
* element is kept.
|
* element is kept.
|
||||||
*
|
*
|
||||||
* @param {Array} arr - The array to inspect
|
* @param {Array} arr - The array to inspect
|
||||||
* @param {Function} fn - The iteratee function to compute uniqueness criterion
|
* @param {Function} fn - The iteratee function to compute uniqueness criterion
|
||||||
* @returns {Array} A new duplicate-free array
|
* @returns {Array} A new duplicate-free array
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const users = [{id: 1, name: 'john'}, {id: 2, name: 'jane'}, {id: 1, name: 'john'}];
|
* const users = [{id: 1, name: 'john'}, {id: 2, name: 'jane'}, {id: 1, name: 'john'}];
|
||||||
* uniqBy(users, x => x.id); // [{id: 1, name: 'john'}, {id: 2, name: 'jane'}]
|
* uniqBy(users, x => x.id); // [{id: 1, name: 'john'}, {id: 2, name: 'jane'}]
|
||||||
*/
|
*/
|
||||||
export const uniqBy = <T>(arr: T[], fn: (item: T) => unknown): T[] =>
|
export const uniqBy = <T>(arr: T[], fn: (item: T) => unknown): T[] =>
|
||||||
arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index)
|
arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a string to start case (capitalizes the first letter of each word).
|
* Converts a string to start case (capitalizes the first letter of each word).
|
||||||
* Handles camelCase, snake_case, kebab-case, and regular spaces.
|
* Handles camelCase, snake_case, kebab-case, and regular spaces.
|
||||||
*
|
*
|
||||||
* @param {string} str - The string to convert
|
* @param {string} str - The string to convert
|
||||||
* @returns {string} The start case string
|
* @returns {string} The start case string
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* startCase('hello_world'); // "Hello World"
|
* startCase('hello_world'); // "Hello World"
|
||||||
* startCase('helloWorld'); // "Hello World"
|
* startCase('helloWorld'); // "Hello World"
|
||||||
* startCase('hello-world'); // "Hello World"
|
* startCase('hello-world'); // "Hello World"
|
||||||
* startCase('hello world'); // "Hello World"
|
* startCase('hello world'); // "Hello World"
|
||||||
*/
|
*/
|
||||||
export const startCase = (str: string): string =>
|
export const startCase = (str: string): string =>
|
||||||
str
|
str
|
||||||
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase
|
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase
|
||||||
.replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces
|
.replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces
|
||||||
.replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word
|
.replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word
|
||||||
|
|||||||
@@ -1,164 +1,164 @@
|
|||||||
import { ChannelData, ChannelSearchableData } from '../types/channel'
|
import { ChannelData, ChannelSearchableData } from '../types/channel'
|
||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Stream, Feed, Logo, GuideChannel } from './'
|
import { Stream, Feed, Logo, GuideChannel } from './'
|
||||||
|
|
||||||
export class Channel {
|
export class Channel {
|
||||||
id?: string
|
id?: string
|
||||||
name?: string
|
name?: string
|
||||||
altNames?: Collection
|
altNames?: Collection
|
||||||
network?: string
|
network?: string
|
||||||
owners?: Collection
|
owners?: Collection
|
||||||
countryCode?: string
|
countryCode?: string
|
||||||
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
|
||||||
website?: string
|
website?: string
|
||||||
feeds?: Collection
|
feeds?: Collection
|
||||||
logos: Collection = new Collection()
|
logos: Collection = new Collection()
|
||||||
|
|
||||||
constructor(data?: ChannelData) {
|
constructor(data?: ChannelData) {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.altNames = new Collection(data.alt_names)
|
this.altNames = new Collection(data.alt_names)
|
||||||
this.network = data.network || undefined
|
this.network = data.network || undefined
|
||||||
this.owners = new Collection(data.owners)
|
this.owners = new Collection(data.owners)
|
||||||
this.countryCode = data.country
|
this.countryCode = data.country
|
||||||
this.subdivisionCode = data.subdivision || undefined
|
this.subdivisionCode = data.subdivision || undefined
|
||||||
this.cityName = data.city || undefined
|
this.cityName = data.city || undefined
|
||||||
this.categoryIds = new Collection(data.categories)
|
this.categoryIds = new Collection(data.categories)
|
||||||
this.isNSFW = data.is_nsfw
|
this.isNSFW = data.is_nsfw
|
||||||
this.launched = data.launched || undefined
|
this.launched = data.launched || undefined
|
||||||
this.closed = data.closed || undefined
|
this.closed = data.closed || undefined
|
||||||
this.replacedBy = data.replaced_by || undefined
|
this.replacedBy = data.replaced_by || undefined
|
||||||
this.website = data.website || undefined
|
this.website = data.website || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
||||||
if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
|
if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withLogos(logosGroupedByChannelId: Dictionary): this {
|
withLogos(logosGroupedByChannelId: Dictionary): this {
|
||||||
if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id))
|
if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeeds(): Collection {
|
getFeeds(): Collection {
|
||||||
if (!this.feeds) return new Collection()
|
if (!this.feeds) return new Collection()
|
||||||
|
|
||||||
return this.feeds
|
return this.feeds
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuideChannels(): Collection {
|
getGuideChannels(): Collection {
|
||||||
let channels = new Collection()
|
let channels = new Collection()
|
||||||
|
|
||||||
this.getFeeds().forEach((feed: Feed) => {
|
this.getFeeds().forEach((feed: Feed) => {
|
||||||
channels = channels.concat(feed.getGuideChannels())
|
channels = channels.concat(feed.getGuideChannels())
|
||||||
})
|
})
|
||||||
|
|
||||||
return channels
|
return channels
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuideChannelNames(): Collection {
|
getGuideChannelNames(): Collection {
|
||||||
return this.getGuideChannels()
|
return this.getGuideChannels()
|
||||||
.map((channel: GuideChannel) => channel.siteName)
|
.map((channel: GuideChannel) => channel.siteName)
|
||||||
.uniq()
|
.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreams(): Collection {
|
getStreams(): Collection {
|
||||||
let streams = new Collection()
|
let streams = new Collection()
|
||||||
|
|
||||||
this.getFeeds().forEach((feed: Feed) => {
|
this.getFeeds().forEach((feed: Feed) => {
|
||||||
streams = streams.concat(feed.getStreams())
|
streams = streams.concat(feed.getStreams())
|
||||||
})
|
})
|
||||||
|
|
||||||
return streams
|
return streams
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamNames(): Collection {
|
getStreamNames(): Collection {
|
||||||
return this.getStreams()
|
return this.getStreams()
|
||||||
.map((stream: Stream) => stream.getName())
|
.map((stream: Stream) => stream.getName())
|
||||||
.uniq()
|
.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeedFullNames(): Collection {
|
getFeedFullNames(): Collection {
|
||||||
return this.getFeeds()
|
return this.getFeeds()
|
||||||
.map((feed: Feed) => feed.getFullName())
|
.map((feed: Feed) => feed.getFullName())
|
||||||
.uniq()
|
.uniq()
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return this.name || ''
|
return this.name || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): string {
|
getId(): string {
|
||||||
return this.id || ''
|
return this.id || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getAltNames(): Collection {
|
getAltNames(): Collection {
|
||||||
return this.altNames || new Collection()
|
return this.altNames || new Collection()
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogos(): Collection {
|
getLogos(): Collection {
|
||||||
function feed(logo: Logo): number {
|
function feed(logo: Logo): number {
|
||||||
if (!logo.feed) return 1
|
if (!logo.feed) return 1
|
||||||
if (logo.feed.isMain) return 1
|
if (logo.feed.isMain) return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
WebP: 1,
|
WebP: 1,
|
||||||
AVIF: 1,
|
AVIF: 1,
|
||||||
JPEG: 2,
|
JPEG: 2,
|
||||||
GIF: 1
|
GIF: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return logo.format ? levelByFormat[logo.format] : 0
|
return logo.format ? levelByFormat[logo.format] : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function size(logo: Logo): number {
|
function size(logo: Logo): number {
|
||||||
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false)
|
return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogo(): Logo | undefined {
|
getLogo(): Logo | undefined {
|
||||||
return this.getLogos().first()
|
return this.getLogos().first()
|
||||||
}
|
}
|
||||||
|
|
||||||
hasLogo(): boolean {
|
hasLogo(): boolean {
|
||||||
return this.getLogos().notEmpty()
|
return this.getLogos().notEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogoUrl(): string {
|
getLogoUrl(): string {
|
||||||
const logo = this.getLogo()
|
const logo = this.getLogo()
|
||||||
if (!logo) return ''
|
if (!logo) return ''
|
||||||
|
|
||||||
return logo.url || ''
|
return logo.url || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchable(): ChannelSearchableData {
|
getSearchable(): ChannelSearchableData {
|
||||||
return {
|
return {
|
||||||
id: this.getId(),
|
id: this.getId(),
|
||||||
name: this.getName(),
|
name: this.getName(),
|
||||||
altNames: this.getAltNames().all(),
|
altNames: this.getAltNames().all(),
|
||||||
guideNames: this.getGuideChannelNames().all(),
|
guideNames: this.getGuideChannelNames().all(),
|
||||||
streamNames: this.getStreamNames().all(),
|
streamNames: this.getStreamNames().all(),
|
||||||
feedFullNames: this.getFeedFullNames().all()
|
feedFullNames: this.getFeedFullNames().all()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
import epgGrabber from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
|
|
||||||
export class ChannelList {
|
export class ChannelList {
|
||||||
channels: Collection = new Collection()
|
channels: Collection = new Collection()
|
||||||
|
|
||||||
constructor(data: { channels: epgGrabber.Channel[] }) {
|
constructor(data: { channels: epgGrabber.Channel[] }) {
|
||||||
this.channels = new Collection(data.channels)
|
this.channels = new Collection(data.channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
add(channel: epgGrabber.Channel): this {
|
add(channel: epgGrabber.Channel): this {
|
||||||
this.channels.add(channel)
|
this.channels.add(channel)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
get(siteId: string): epgGrabber.Channel | undefined {
|
get(siteId: string): epgGrabber.Channel | undefined {
|
||||||
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
|
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort(): this {
|
sort(): this {
|
||||||
this.channels = this.channels.orderBy([
|
this.channels = this.channels.orderBy([
|
||||||
(channel: epgGrabber.Channel) => channel.lang || '_',
|
(channel: epgGrabber.Channel) => channel.lang || '_',
|
||||||
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
|
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
|
||||||
(channel: epgGrabber.Channel) => channel.site_id
|
(channel: epgGrabber.Channel) => channel.site_id
|
||||||
])
|
])
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
|
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
|
||||||
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
|
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
|
||||||
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
|
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
|
||||||
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
|
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
|
||||||
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
|
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
|
||||||
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
|
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
|
||||||
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
|
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
|
||||||
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
|
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
|
||||||
'g'
|
'g'
|
||||||
)
|
)
|
||||||
|
|
||||||
value = String(value || '').replace(regex, '')
|
value = String(value || '').replace(regex, '')
|
||||||
|
|
||||||
return value
|
return value
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''')
|
.replace(/'/g, ''')
|
||||||
.replace(/\n|\r/g, ' ')
|
.replace(/\n|\r/g, ' ')
|
||||||
.replace(/ +/g, ' ')
|
.replace(/ +/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
|
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
|
||||||
|
|
||||||
this.channels.forEach((channel: epgGrabber.Channel) => {
|
this.channels.forEach((channel: epgGrabber.Channel) => {
|
||||||
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
|
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
|
||||||
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
|
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
|
||||||
const lang = channel.lang || ''
|
const lang = channel.lang || ''
|
||||||
const site_id = channel.site_id || ''
|
const site_id = channel.site_id || ''
|
||||||
const site = channel.site || ''
|
const site = channel.site || ''
|
||||||
const displayName = channel.name ? escapeString(channel.name) : ''
|
const displayName = channel.name ? escapeString(channel.name) : ''
|
||||||
|
|
||||||
output += ` <channel site="${site}" lang="${lang}" xmltv_id="${xmltv_id}" site_id="${site_id}"${logo}>${displayName}</channel>\r\n`
|
output += ` <channel site="${site}" lang="${lang}" xmltv_id="${xmltv_id}" site_id="${site_id}"${logo}>${displayName}</channel>\r\n`
|
||||||
})
|
})
|
||||||
|
|
||||||
output += '</channels>\r\n'
|
output += '</channels>\r\n'
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,124 @@
|
|||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { FeedData } from '../types/feed'
|
import { FeedData } from '../types/feed'
|
||||||
import { Logo, Channel } from '.'
|
import { Logo, Channel } from '.'
|
||||||
|
|
||||||
export class Feed {
|
export class Feed {
|
||||||
channelId: string
|
channelId: string
|
||||||
channel?: Channel
|
channel?: Channel
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
isMain: boolean
|
isMain: boolean
|
||||||
broadcastAreaCodes: Collection
|
broadcastAreaCodes: Collection
|
||||||
languageCodes: Collection
|
languageCodes: Collection
|
||||||
timezoneIds: Collection
|
timezoneIds: Collection
|
||||||
videoFormat: string
|
videoFormat: string
|
||||||
guideChannels?: Collection
|
guideChannels?: Collection
|
||||||
streams?: Collection
|
streams?: Collection
|
||||||
logos: Collection = new Collection()
|
logos: Collection = new Collection()
|
||||||
|
|
||||||
constructor(data: FeedData) {
|
constructor(data: FeedData) {
|
||||||
this.channelId = data.channel
|
this.channelId = data.channel
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.isMain = data.is_main
|
this.isMain = data.is_main
|
||||||
this.broadcastAreaCodes = new Collection(data.broadcast_area)
|
this.broadcastAreaCodes = new Collection(data.broadcast_area)
|
||||||
this.languageCodes = new Collection(data.languages)
|
this.languageCodes = new Collection(data.languages)
|
||||||
this.timezoneIds = new Collection(data.timezones)
|
this.timezoneIds = new Collection(data.timezones)
|
||||||
this.videoFormat = data.video_format
|
this.videoFormat = data.video_format
|
||||||
}
|
}
|
||||||
|
|
||||||
withChannel(channelsKeyById: Dictionary): this {
|
withChannel(channelsKeyById: Dictionary): this {
|
||||||
this.channel = channelsKeyById.get(this.channelId)
|
this.channel = channelsKeyById.get(this.channelId)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withStreams(streamsGroupedById: Dictionary): this {
|
withStreams(streamsGroupedById: Dictionary): this {
|
||||||
this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`))
|
this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`))
|
||||||
|
|
||||||
if (this.isMain) {
|
if (this.isMain) {
|
||||||
this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId)))
|
this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
|
withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
|
||||||
this.guideChannels = new Collection(
|
this.guideChannels = new Collection(
|
||||||
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
|
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.isMain) {
|
if (this.isMain) {
|
||||||
this.guideChannels = this.guideChannels.concat(
|
this.guideChannels = this.guideChannels.concat(
|
||||||
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
|
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withLogos(logosGroupedByStreamId: Dictionary): this {
|
withLogos(logosGroupedByStreamId: Dictionary): this {
|
||||||
this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
|
this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuideChannels(): Collection {
|
getGuideChannels(): Collection {
|
||||||
if (!this.guideChannels) return new Collection()
|
if (!this.guideChannels) return new Collection()
|
||||||
|
|
||||||
return this.guideChannels
|
return this.guideChannels
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreams(): Collection {
|
getStreams(): Collection {
|
||||||
if (!this.streams) return new Collection()
|
if (!this.streams) return new Collection()
|
||||||
|
|
||||||
return this.streams
|
return this.streams
|
||||||
}
|
}
|
||||||
|
|
||||||
getFullName(): string {
|
getFullName(): string {
|
||||||
if (!this.channel) return ''
|
if (!this.channel) return ''
|
||||||
|
|
||||||
return `${this.channel.name} ${this.name}`
|
return `${this.channel.name} ${this.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamId(): string {
|
getStreamId(): string {
|
||||||
return `${this.channelId}@${this.id}`
|
return `${this.channelId}@${this.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
WebP: 1,
|
WebP: 1,
|
||||||
AVIF: 1,
|
AVIF: 1,
|
||||||
JPEG: 2,
|
JPEG: 2,
|
||||||
GIF: 1
|
GIF: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return logo.format ? levelByFormat[logo.format] : 0
|
return logo.format ? levelByFormat[logo.format] : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function size(logo: Logo): number {
|
function size(logo: Logo): number {
|
||||||
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
|
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogo(): Logo | undefined {
|
getLogo(): Logo | undefined {
|
||||||
return this.getLogos().first()
|
return this.getLogos().first()
|
||||||
}
|
}
|
||||||
|
|
||||||
hasLogo(): boolean {
|
hasLogo(): boolean {
|
||||||
return this.getLogos().notEmpty()
|
return this.getLogos().notEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogoUrl(): string {
|
getLogoUrl(): string {
|
||||||
const logo = this.getLogo()
|
const logo = this.getLogo()
|
||||||
if (!logo) return ''
|
if (!logo) return ''
|
||||||
|
|
||||||
return logo.url || ''
|
return logo.url || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
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
|
||||||
gzip: boolean
|
gzip: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Guide {
|
export class Guide {
|
||||||
channels: Collection
|
channels: Collection
|
||||||
programs: Collection
|
programs: Collection
|
||||||
filepath: string
|
filepath: string
|
||||||
gzip: boolean
|
gzip: boolean
|
||||||
|
|
||||||
constructor({ channels, programs, filepath, gzip }: GuideData) {
|
constructor({ channels, programs, filepath, gzip }: GuideData) {
|
||||||
this.channels = channels
|
this.channels = channels
|
||||||
this.programs = programs
|
this.programs = programs
|
||||||
this.filepath = filepath
|
this.filepath = filepath
|
||||||
this.gzip = gzip || false
|
this.gzip = gzip || false
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
|
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
|
||||||
timezone: 'UTC'
|
timezone: 'UTC'
|
||||||
})
|
})
|
||||||
|
|
||||||
return generateXMLTV({
|
return generateXMLTV({
|
||||||
channels: this.channels.all(),
|
channels: this.channels.all(),
|
||||||
programs: this.programs.all(),
|
programs: this.programs.all(),
|
||||||
date: currDate.toJSON()
|
date: currDate.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Issue {
|
export class Issue {
|
||||||
number: number
|
number: number
|
||||||
labels: string[]
|
labels: string[]
|
||||||
data: Dictionary
|
data: Dictionary
|
||||||
|
|
||||||
constructor({ number, labels, data }: IssueProps) {
|
constructor({ number, labels, data }: IssueProps) {
|
||||||
this.number = number
|
this.number = number
|
||||||
this.labels = labels
|
this.labels = labels
|
||||||
this.data = data
|
this.data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
getURL() {
|
getURL() {
|
||||||
return `https://github.com/${OWNER}/${REPO}/issues/${this.number}`
|
return `https://github.com/${OWNER}/${REPO}/issues/${this.number}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import { Collection, type Dictionary } from '@freearhey/core'
|
import { Collection, type Dictionary } from '@freearhey/core'
|
||||||
import type { LogoData } from '../types/logo'
|
import type { LogoData } from '../types/logo'
|
||||||
import { type Feed } from './feed'
|
import { type Feed } from './feed'
|
||||||
|
|
||||||
export class Logo {
|
export class Logo {
|
||||||
channelId?: string
|
channelId?: string
|
||||||
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
|
||||||
|
|
||||||
constructor(data?: LogoData) {
|
constructor(data?: LogoData) {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
this.channelId = data.channel
|
this.channelId = data.channel
|
||||||
this.feedId = data.feed || undefined
|
this.feedId = data.feed || undefined
|
||||||
this.tags = new Collection(data.tags)
|
this.tags = new Collection(data.tags)
|
||||||
this.width = data.width
|
this.width = data.width
|
||||||
this.height = data.height
|
this.height = data.height
|
||||||
this.format = data.format || undefined
|
this.format = data.format || undefined
|
||||||
this.url = data.url
|
this.url = data.url
|
||||||
}
|
}
|
||||||
|
|
||||||
withFeed(feedsKeyByStreamId: Dictionary): this {
|
withFeed(feedsKeyByStreamId: Dictionary): this {
|
||||||
if (!this.feedId) return this
|
if (!this.feedId) return this
|
||||||
|
|
||||||
this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
this.feed = feedsKeyByStreamId.get(this.getStreamId())
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getStreamId(): string {
|
getStreamId(): string {
|
||||||
if (!this.channelId) return ''
|
if (!this.channelId) return ''
|
||||||
if (!this.feedId) return this.channelId
|
if (!this.feedId) return this.channelId
|
||||||
|
|
||||||
return `${this.channelId}@${this.feedId}`
|
return `${this.channelId}@${this.feedId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
import { Collection } from '@freearhey/core'
|
||||||
import { Issue } from './'
|
import { Issue } from './'
|
||||||
|
|
||||||
enum StatusCode {
|
enum StatusCode {
|
||||||
DOWN = 'down',
|
DOWN = 'down',
|
||||||
WARNING = 'warning',
|
WARNING = 'warning',
|
||||||
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
|
||||||
issues: Collection
|
issues: Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Site {
|
export class Site {
|
||||||
domain: string
|
domain: string
|
||||||
totalChannels: number
|
totalChannels: number
|
||||||
markedChannels: number
|
markedChannels: number
|
||||||
issues: Collection
|
issues: Collection
|
||||||
|
|
||||||
constructor({ domain, totalChannels = 0, markedChannels = 0, issues }: SiteProps) {
|
constructor({ domain, totalChannels = 0, markedChannels = 0, issues }: SiteProps) {
|
||||||
this.domain = domain
|
this.domain = domain
|
||||||
this.totalChannels = totalChannels
|
this.totalChannels = totalChannels
|
||||||
this.markedChannels = markedChannels
|
this.markedChannels = markedChannels
|
||||||
this.issues = issues
|
this.issues = issues
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): Status {
|
getStatus(): Status {
|
||||||
const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
|
const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
|
||||||
issue.labels.find(label => label === 'status:down')
|
issue.labels.find(label => label === 'status:down')
|
||||||
)
|
)
|
||||||
if (issuesWithStatusDown.notEmpty())
|
if (issuesWithStatusDown.notEmpty())
|
||||||
return {
|
return {
|
||||||
code: StatusCode.DOWN,
|
code: StatusCode.DOWN,
|
||||||
emoji: '🔴'
|
emoji: '🔴'
|
||||||
}
|
}
|
||||||
|
|
||||||
const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
|
const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
|
||||||
issue.labels.find(label => label === 'status:warning')
|
issue.labels.find(label => label === 'status:warning')
|
||||||
)
|
)
|
||||||
if (issuesWithStatusWarning.notEmpty())
|
if (issuesWithStatusWarning.notEmpty())
|
||||||
return {
|
return {
|
||||||
code: StatusCode.WARNING,
|
code: StatusCode.WARNING,
|
||||||
emoji: '🟡'
|
emoji: '🟡'
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: StatusCode.OK,
|
code: StatusCode.OK,
|
||||||
emoji: '🟢'
|
emoji: '🟢'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getIssues(): Collection {
|
getIssues(): Collection {
|
||||||
return this.issues.map((issue: Issue) => issue.getURL())
|
return this.issues.map((issue: Issue) => issue.getURL())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
import type { StreamData } from '../types/stream'
|
import type { StreamData } from '../types/stream'
|
||||||
import { Feed, Channel } from './index'
|
import { Feed, Channel } from './index'
|
||||||
|
|
||||||
export class Stream {
|
export class Stream {
|
||||||
name?: string
|
name?: string
|
||||||
url: string
|
url: string
|
||||||
id?: string
|
id?: string
|
||||||
channelId?: string
|
channelId?: string
|
||||||
channel?: Channel
|
channel?: Channel
|
||||||
feedId?: string
|
feedId?: string
|
||||||
feed?: Feed
|
feed?: Feed
|
||||||
filepath?: string
|
filepath?: string
|
||||||
line?: number
|
line?: number
|
||||||
label?: string
|
label?: string
|
||||||
verticalResolution?: number
|
verticalResolution?: number
|
||||||
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
|
||||||
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
|
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
|
||||||
|
|
||||||
this.id = id || undefined
|
this.id = id || undefined
|
||||||
this.channelId = data.channel || undefined
|
this.channelId = data.channel || undefined
|
||||||
this.feedId = data.feed || undefined
|
this.feedId = data.feed || undefined
|
||||||
this.name = data.name || undefined
|
this.name = data.name || undefined
|
||||||
this.url = data.url
|
this.url = data.url
|
||||||
this.referrer = data.referrer || undefined
|
this.referrer = data.referrer || undefined
|
||||||
this.userAgent = data.user_agent || undefined
|
this.userAgent = data.user_agent || undefined
|
||||||
this.verticalResolution = verticalResolution || undefined
|
this.verticalResolution = verticalResolution || undefined
|
||||||
this.isInterlaced = isInterlaced || undefined
|
this.isInterlaced = isInterlaced || undefined
|
||||||
this.label = data.label || undefined
|
this.label = data.label || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): string {
|
getId(): string {
|
||||||
return this.id || ''
|
return this.id || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return this.name || ''
|
return this.name || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseQuality(quality: string | null): {
|
function parseQuality(quality: string | null): {
|
||||||
verticalResolution: number | null
|
verticalResolution: number | null
|
||||||
isInterlaced: boolean | null
|
isInterlaced: boolean | null
|
||||||
} {
|
} {
|
||||||
if (!quality) return { verticalResolution: null, isInterlaced: null }
|
if (!quality) return { verticalResolution: null, isInterlaced: null }
|
||||||
const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
|
const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
|
||||||
const isInterlaced = /i$/i.test(quality)
|
const isInterlaced = /i$/i.test(quality)
|
||||||
let verticalResolution = 0
|
let verticalResolution = 0
|
||||||
if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString)
|
if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString)
|
||||||
|
|
||||||
return { verticalResolution, isInterlaced }
|
return { verticalResolution, isInterlaced }
|
||||||
}
|
}
|
||||||
|
|||||||
54
scripts/types/channel.d.ts
vendored
54
scripts/types/channel.d.ts
vendored
@@ -1,27 +1,27 @@
|
|||||||
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[]
|
||||||
network: string
|
network: string
|
||||||
owners: Collection
|
owners: Collection
|
||||||
country: string
|
country: string
|
||||||
subdivision: string
|
subdivision: string
|
||||||
city: string
|
city: string
|
||||||
categories: Collection
|
categories: Collection
|
||||||
is_nsfw: boolean
|
is_nsfw: boolean
|
||||||
launched: string
|
launched: string
|
||||||
closed: string
|
closed: string
|
||||||
replaced_by: string
|
replaced_by: string
|
||||||
website: string
|
website: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChannelSearchableData = {
|
export interface ChannelSearchableData {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
altNames: string[]
|
altNames: string[]
|
||||||
guideNames: string[]
|
guideNames: string[]
|
||||||
streamNames: string[]
|
streamNames: string[]
|
||||||
feedFullNames: string[]
|
feedFullNames: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
40
scripts/types/dataLoader.d.ts
vendored
40
scripts/types/dataLoader.d.ts
vendored
@@ -1,20 +1,20 @@
|
|||||||
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[]
|
||||||
languages: object | object[]
|
languages: object | object[]
|
||||||
categories: object | object[]
|
categories: object | object[]
|
||||||
blocklist: object | object[]
|
blocklist: object | object[]
|
||||||
channels: object | object[]
|
channels: object | object[]
|
||||||
feeds: object | object[]
|
feeds: object | object[]
|
||||||
timezones: object | object[]
|
timezones: object | object[]
|
||||||
guides: object | object[]
|
guides: object | object[]
|
||||||
streams: object | object[]
|
streams: object | object[]
|
||||||
logos: object | object[]
|
logos: object | object[]
|
||||||
}
|
}
|
||||||
|
|||||||
32
scripts/types/dataProcessor.d.ts
vendored
32
scripts/types/dataProcessor.d.ts
vendored
@@ -1,16 +1,16 @@
|
|||||||
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
|
||||||
logosGroupedByStreamId: Dictionary
|
logosGroupedByStreamId: Dictionary
|
||||||
feedsKeyByStreamId: Dictionary
|
feedsKeyByStreamId: Dictionary
|
||||||
streamsGroupedById: Dictionary
|
streamsGroupedById: Dictionary
|
||||||
channelsKeyById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
guideChannels: Collection
|
guideChannels: Collection
|
||||||
channels: Collection
|
channels: Collection
|
||||||
streams: Collection
|
streams: Collection
|
||||||
feeds: Collection
|
feeds: Collection
|
||||||
logos: Collection
|
logos: Collection
|
||||||
}
|
}
|
||||||
|
|||||||
24
scripts/types/feed.d.ts
vendored
24
scripts/types/feed.d.ts
vendored
@@ -1,12 +1,12 @@
|
|||||||
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
|
||||||
is_main: boolean
|
is_main: boolean
|
||||||
broadcast_area: Collection
|
broadcast_area: Collection
|
||||||
languages: Collection
|
languages: Collection
|
||||||
timezones: Collection
|
timezones: Collection
|
||||||
video_format: string
|
video_format: string
|
||||||
}
|
}
|
||||||
|
|||||||
16
scripts/types/guide.d.ts
vendored
16
scripts/types/guide.d.ts
vendored
@@ -1,8 +1,8 @@
|
|||||||
export type GuideData = {
|
export interface GuideData {
|
||||||
channel: string
|
channel: string
|
||||||
feed: string
|
feed: string
|
||||||
site: string
|
site: string
|
||||||
site_id: string
|
site_id: string
|
||||||
site_name: string
|
site_name: string
|
||||||
lang: string
|
lang: string
|
||||||
}
|
}
|
||||||
|
|||||||
18
scripts/types/logo.d.ts
vendored
18
scripts/types/logo.d.ts
vendored
@@ -1,9 +1,9 @@
|
|||||||
export type LogoData = {
|
export interface LogoData {
|
||||||
channel: string
|
channel: string
|
||||||
feed: string | null
|
feed: string | null
|
||||||
tags: string[]
|
tags: string[]
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
format: string | null
|
format: string | null
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|||||||
20
scripts/types/stream.d.ts
vendored
20
scripts/types/stream.d.ts
vendored
@@ -1,10 +1,10 @@
|
|||||||
export type StreamData = {
|
export interface StreamData {
|
||||||
channel: string | null
|
channel: string | null
|
||||||
feed: string | null
|
feed: string | null
|
||||||
name?: string
|
name?: string
|
||||||
url: string
|
url: string
|
||||||
referrer: string | null
|
referrer: string | null
|
||||||
user_agent: string | null
|
user_agent: string | null
|
||||||
quality: string | null
|
quality: string | null
|
||||||
label: string | null
|
label: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
const { parser, url } = require('./tvim.tv.config.js')
|
const { parser, url } = require('./tvim.tv.config.js')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2021-10-24', 'YYYY-MM-DD').startOf('d')
|
||||||
const channel = { site_id: 'T7', xmltv_id: 'T7.rs' }
|
const channel = { site_id: 'T7', xmltv_id: 'T7.rs' }
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
const result = url({ date, channel })
|
const result = url({ date, channel })
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true'
|
'https://www.tvim.tv/script/program_epg?date=24.10.2021&prog=T7&server_time=true'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const result = parser({ date, channel, content })
|
const result = parser({ date, channel, content })
|
||||||
expect(result).toMatchObject([
|
expect(result).toMatchObject([
|
||||||
{
|
{
|
||||||
start: 'Sat, 23 Oct 2021 22:00:00 GMT',
|
start: 'Sat, 23 Oct 2021 22:00:00 GMT',
|
||||||
stop: 'Sun, 24 Oct 2021 02:00:00 GMT',
|
stop: 'Sun, 24 Oct 2021 02:00:00 GMT',
|
||||||
title: 'Programi i T7',
|
title: 'Programi i T7',
|
||||||
description: 'Programi i T7',
|
description: 'Programi i T7',
|
||||||
category: 'test'
|
category: 'test'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const result = parser({
|
const result = parser({
|
||||||
date,
|
date,
|
||||||
channel,
|
channel,
|
||||||
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
|
||||||
})
|
})
|
||||||
expect(result).toMatchObject([])
|
expect(result).toMatchObject([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,127 +1,127 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const { DateTime } = require('luxon')
|
const { DateTime } = require('luxon')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'tvinsider.com',
|
site: 'tvinsider.com',
|
||||||
days: 2,
|
days: 2,
|
||||||
url({ channel }) {
|
url({ channel }) {
|
||||||
return `https://www.tvinsider.com/network/${channel.site_id}/schedule/`
|
return `https://www.tvinsider.com/network/${channel.site_id}/schedule/`
|
||||||
},
|
},
|
||||||
parser({ content, date }) {
|
parser({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content, date)
|
const items = parseItems(content, date)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
const episodeInfo = parseEP($item)
|
const episodeInfo = parseEP($item)
|
||||||
let start = parseStart($item, date)
|
let start = parseStart($item, date)
|
||||||
if (!start) return
|
if (!start) return
|
||||||
if (prev) {
|
if (prev) {
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.plus({ minute: 30 })
|
const stop = start.plus({ minute: 30 })
|
||||||
|
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
description: parseDescription($item),
|
description: parseDescription($item),
|
||||||
category: parseCategory($item),
|
category: parseCategory($item),
|
||||||
date: parseDate($item),
|
date: parseDate($item),
|
||||||
...episodeInfo,
|
...episodeInfo,
|
||||||
subTitles: parseSubtitle($item),
|
subTitles: parseSubtitle($item),
|
||||||
previouslyShown: parsePreviously($item),
|
previouslyShown: parsePreviously($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const html = await axios
|
const html = await axios
|
||||||
.get('https://www.tvinsider.com/network/5-star-max/')
|
.get('https://www.tvinsider.com/network/5-star-max/')
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
const $ = cheerio.load(html)
|
const $ = cheerio.load(html)
|
||||||
const items = $('body > main > section > select > option').toArray()
|
const items = $('body > main > section > select > option').toArray()
|
||||||
|
|
||||||
const channels = []
|
const channels = []
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const name = $(item).text().trim()
|
const name = $(item).text().trim()
|
||||||
const path = $(item).attr('value')
|
const path = $(item).attr('value')
|
||||||
if (!path) return
|
if (!path) return
|
||||||
const [, , site_id] = path.split('/') || [null, null, null]
|
const [, , site_id] = path.split('/') || [null, null, null]
|
||||||
if (!site_id) return
|
if (!site_id) return
|
||||||
|
|
||||||
channels.push({
|
channels.push({
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
site_id,
|
site_id,
|
||||||
name
|
name
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return channels
|
return channels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('h3').text().trim()
|
return $item('h3').text().trim()
|
||||||
}
|
}
|
||||||
function parseEP($item){
|
function parseEP($item){
|
||||||
const text = $item('h6').text().trim()
|
const text = $item('h6').text().trim()
|
||||||
const match = text.match(/Season\s+(\d+)\s*•\s*Episode\s+(\d+)/i)
|
const match = text.match(/Season\s+(\d+)\s*•\s*Episode\s+(\d+)/i)
|
||||||
|
|
||||||
if (!match) return {} // Return an empty object if no match, so properties are undefined later
|
if (!match) return {} // Return an empty object if no match, so properties are undefined later
|
||||||
|
|
||||||
const season = parseInt(match[1], 10)
|
const season = parseInt(match[1], 10)
|
||||||
const episode = parseInt(match[2], 10)
|
const episode = parseInt(match[2], 10)
|
||||||
|
|
||||||
return { season, episode } // Return an object with season and episode
|
return { season, episode } // Return an object with season and episode
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSubtitle($item) {
|
function parseSubtitle($item) {
|
||||||
return $item('h5').text().trim()
|
return $item('h5').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePreviously($item){
|
function parsePreviously($item){
|
||||||
const h3Text = $item('h3').text().trim()
|
const h3Text = $item('h3').text().trim()
|
||||||
const isNewShow = /New$/.test(h3Text)
|
const isNewShow = /New$/.test(h3Text)
|
||||||
|
|
||||||
if (isNewShow) {
|
if (isNewShow) {
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDescription($item) {
|
function parseDescription($item) {
|
||||||
return $item('p').text().trim()
|
return $item('p').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategory($item) {
|
function parseCategory($item) {
|
||||||
const [category] = $item('h4').text().trim().split(' • ')
|
const [category] = $item('h4').text().trim().split(' • ')
|
||||||
|
|
||||||
return category
|
return category
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDate($item) {
|
function parseDate($item) {
|
||||||
const [, date] = $item('h4').text().trim().split(' • ')
|
const [, date] = $item('h4').text().trim().split(' • ')
|
||||||
|
|
||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
let time = $item('time').text().trim()
|
let time = $item('time').text().trim()
|
||||||
time = `${date.format('YYYY-MM-DD')} ${time}`
|
time = `${date.format('YYYY-MM-DD')} ${time}`
|
||||||
|
|
||||||
return DateTime.fromFormat(time, 'yyyy-MM-dd t', { zone: 'America/New_York' }).toUTC()
|
return DateTime.fromFormat(time, 'yyyy-MM-dd t', { zone: 'America/New_York' }).toUTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, date) {
|
function parseItems(content, date) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $(`#${date.format('MM-DD-YYYY')}`)
|
return $(`#${date.format('MM-DD-YYYY')}`)
|
||||||
.next()
|
.next()
|
||||||
.find('a')
|
.find('a')
|
||||||
.toArray()
|
.toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,99 @@
|
|||||||
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('../../scripts/functions')
|
const { uniqBy } = require('../../scripts/functions')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'tvireland.ie',
|
site: 'tvireland.ie',
|
||||||
days: 2,
|
days: 2,
|
||||||
url: function ({ date, channel }) {
|
url: function ({ date, channel }) {
|
||||||
return `https://www.tvireland.ie/tv/listings/channel/${channel.site_id}?dt=${date.format(
|
return `https://www.tvireland.ie/tv/listings/channel/${channel.site_id}?dt=${date.format(
|
||||||
'YYYY-MM-DD'
|
'YYYY-MM-DD'
|
||||||
)}`
|
)}`
|
||||||
},
|
},
|
||||||
parser: function ({ content, date, channel }) {
|
parser: function ({ content, date, channel }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content)
|
const items = parseItems(content)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
let start = parseStart($item, date, channel)
|
let start = parseStart($item, date, channel)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
if (start.isBefore(prev.start)) {
|
if (start.isBefore(prev.start)) {
|
||||||
start = start.add(1, 'd')
|
start = start.add(1, 'd')
|
||||||
date = date.add(1, 'd')
|
date = date.add(1, 'd')
|
||||||
}
|
}
|
||||||
prev.stop = start
|
prev.stop = start
|
||||||
}
|
}
|
||||||
const stop = start.add(30, 'm')
|
const stop = start.add(30, 'm')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
const providers = ['-9000019', '-8000019', '-1000019', '-2000019', '-7000019']
|
const providers = ['-9000019', '-8000019', '-1000019', '-2000019', '-7000019']
|
||||||
|
|
||||||
const channels = []
|
const channels = []
|
||||||
for (let provider of providers) {
|
for (let provider of providers) {
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.post('https://www.tvireland.ie/tv/schedule', null, {
|
.post('https://www.tvireland.ie/tv/schedule', null, {
|
||||||
params: {
|
params: {
|
||||||
provider,
|
provider,
|
||||||
region: 'Ireland',
|
region: 'Ireland',
|
||||||
TVperiod: 'Night',
|
TVperiod: 'Night',
|
||||||
date: dayjs().format('YYYY-MM-DD'),
|
date: dayjs().format('YYYY-MM-DD'),
|
||||||
st: 0,
|
st: 0,
|
||||||
u_time: 2027,
|
u_time: 2027,
|
||||||
is_mobile: 1
|
is_mobile: 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
const $ = cheerio.load(data)
|
const $ = cheerio.load(data)
|
||||||
$('.channelname').each((i, el) => {
|
$('.channelname').each((i, el) => {
|
||||||
const name = $(el).find('center > a:eq(1)').text()
|
const name = $(el).find('center > a:eq(1)').text()
|
||||||
const url = $(el).find('center > a:eq(1)').attr('href')
|
const url = $(el).find('center > a:eq(1)').attr('href')
|
||||||
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
|
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
|
||||||
|
|
||||||
channels.push({
|
channels.push({
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
name,
|
name,
|
||||||
site_id: `${number}/${slug}`
|
site_id: `${number}/${slug}`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return uniqBy(channels, x => x.site_id)
|
return uniqBy(channels, x => x.site_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
const timeString = $item('td:eq(0)').text().trim()
|
const timeString = $item('td:eq(0)').text().trim()
|
||||||
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
|
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`
|
||||||
|
|
||||||
return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', 'Europe/Dublin')
|
return dayjs.tz(dateString, 'YYYY-MM-DD H:mm a', 'Europe/Dublin')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('td:eq(1)').text().trim()
|
return $item('td:eq(1)').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $('table.table > tbody > tr').toArray()
|
return $('table.table > tbody > tr').toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,81 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const { uniqBy } = require('../../scripts/functions')
|
const { uniqBy } = require('../../scripts/functions')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'tvmusor.hu',
|
site: 'tvmusor.hu',
|
||||||
days: 2,
|
days: 2,
|
||||||
url: 'https://tvmusor.borsonline.hu/a/get-events/',
|
url: 'https://tvmusor.borsonline.hu/a/get-events/',
|
||||||
request: {
|
request: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||||
},
|
},
|
||||||
data({ channel, date }) {
|
data({ channel, date }) {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.append(
|
params.append(
|
||||||
'data',
|
'data',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`]
|
blocks: [`${channel.site_id}|${date.format('YYYY-MM-DD')}`]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parser({ content, channel, date }) {
|
parser({ content, channel, date }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel, date)
|
const items = parseItems(content, channel, date)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
let start = dayjs(item.e)
|
let start = dayjs(item.e)
|
||||||
let stop = dayjs(item.f)
|
let stop = dayjs(item.f)
|
||||||
if (prev) {
|
if (prev) {
|
||||||
start = prev.stop
|
start = prev.stop
|
||||||
}
|
}
|
||||||
|
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.j,
|
title: item.j,
|
||||||
category: item.h,
|
category: item.h,
|
||||||
description: item.c,
|
description: item.c,
|
||||||
image: parseImage(item),
|
image: parseImage(item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get('https://tvmusor.borsonline.hu/most/')
|
.get('https://tvmusor.borsonline.hu/most/')
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/)
|
const [, channelData] = data.match(/const CHANNEL_DATA = (.*);/)
|
||||||
const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":')
|
const json = channelData.replace('},}', '}}').replace(/(\d+):/g, '"$1":')
|
||||||
const channels = JSON.parse(json)
|
const channels = JSON.parse(json)
|
||||||
|
|
||||||
return Object.values(channels).map(item => {
|
return Object.values(channels).map(item => {
|
||||||
return {
|
return {
|
||||||
lang: 'hu',
|
lang: 'hu',
|
||||||
site_id: item.id,
|
site_id: item.id,
|
||||||
name: item.name
|
name: item.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseImage(item) {
|
function parseImage(item) {
|
||||||
return item.z ? `https://tvmusor.borsonline.hu/images/events/408/${item.z}` : null
|
return item.z ? `https://tvmusor.borsonline.hu/images/events/408/${item.z}` : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content, channel, date) {
|
function parseItems(content, channel, date) {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data || !data.data || !data.data.loadedBlocks) return []
|
if (!data || !data.data || !data.data.loadedBlocks) return []
|
||||||
const blocks = data.data.loadedBlocks
|
const blocks = data.data.loadedBlocks
|
||||||
const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}`
|
const blockId = `${channel.site_id}_${date.format('YYYY-MM-DD')}`
|
||||||
if (!Array.isArray(blocks[blockId])) return []
|
if (!Array.isArray(blocks[blockId])) return []
|
||||||
|
|
||||||
return uniqBy(uniqBy(blocks[blockId], a => a.e), b => b.b)
|
return uniqBy(uniqBy(blocks[blockId], a => a.e), b => b.b)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,213 +1,213 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
let X_CSRFTOKEN
|
let X_CSRFTOKEN
|
||||||
let Cookie
|
let Cookie
|
||||||
const cookiesToExtract = ['JSESSIONID', 'CSESSIONID', 'CSRFSESSION']
|
const cookiesToExtract = ['JSESSIONID', 'CSESSIONID', 'CSRFSESSION']
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'web.magentatv.de',
|
site: 'web.magentatv.de',
|
||||||
days: 2,
|
days: 2,
|
||||||
url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList',
|
url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList',
|
||||||
request: {
|
request: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
async headers() {
|
async headers() {
|
||||||
return await setHeaders()
|
return await setHeaders()
|
||||||
},
|
},
|
||||||
data({ channel, date }) {
|
data({ channel, date }) {
|
||||||
return {
|
return {
|
||||||
count: -1,
|
count: -1,
|
||||||
isFillProgram: 1,
|
isFillProgram: 1,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
include:
|
include:
|
||||||
'endtime,genres,id,name,starttime,channelid,pictures,introduce,subName,seasonNum,subNum,cast,country,producedate,externalIds',
|
'endtime,genres,id,name,starttime,channelid,pictures,introduce,subName,seasonNum,subNum,cast,country,producedate,externalIds',
|
||||||
name: 'playbill'
|
name: 'playbill'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
type: 2,
|
type: 2,
|
||||||
begintime: date.format('YYYYMMDD000000'),
|
begintime: date.format('YYYYMMDD000000'),
|
||||||
channelid: channel.site_id,
|
channelid: channel.site_id,
|
||||||
endtime: date.add(1, 'd').format('YYYYMMDD000000')
|
endtime: date.add(1, 'd').format('YYYYMMDD000000')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parser({ content }) {
|
parser({ content }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content)
|
const items = parseItems(content)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.name,
|
title: item.name,
|
||||||
description: item.introduce,
|
description: item.introduce,
|
||||||
image: parseImage(item),
|
image: parseImage(item),
|
||||||
category: parseCategory(item),
|
category: parseCategory(item),
|
||||||
start: parseStart(item),
|
start: parseStart(item),
|
||||||
stop: parseStop(item),
|
stop: parseStop(item),
|
||||||
sub_title: item.subName,
|
sub_title: item.subName,
|
||||||
season: item.seasonNum,
|
season: item.seasonNum,
|
||||||
episode: item.subNum,
|
episode: item.subNum,
|
||||||
directors: parseDirectors(item),
|
directors: parseDirectors(item),
|
||||||
producers: parseProducers(item),
|
producers: parseProducers(item),
|
||||||
adapters: parseAdapters(item),
|
adapters: parseAdapters(item),
|
||||||
country: item.country?.toUpperCase(),
|
country: item.country?.toUpperCase(),
|
||||||
date: item.producedate,
|
date: item.producedate,
|
||||||
urls: parseUrls(item)
|
urls: parseUrls(item)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels() {
|
||||||
const url = 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel'
|
const url = 'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel'
|
||||||
const body = {
|
const body = {
|
||||||
channelNamespace: 2,
|
channelNamespace: 2,
|
||||||
filterlist: [
|
filterlist: [
|
||||||
{
|
{
|
||||||
key: 'IsHide',
|
key: 'IsHide',
|
||||||
value: '-1'
|
value: '-1'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
metaDataVer: 'Channel/1.1',
|
metaDataVer: 'Channel/1.1',
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name',
|
include: '/channellist/logicalChannel/contentId,/channellist/logicalChannel/name',
|
||||||
name: 'logicalChannel'
|
name: 'logicalChannel'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
returnSatChannel: 0
|
returnSatChannel: 0
|
||||||
}
|
}
|
||||||
const params = {
|
const params = {
|
||||||
headers: await setHeaders()
|
headers: await setHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.post(url, body, params)
|
.post(url, body, params)
|
||||||
.then(r => r.data)
|
.then(r => r.data)
|
||||||
.catch(console.log)
|
.catch(console.log)
|
||||||
|
|
||||||
return data.channellist.map(item => {
|
return data.channellist.map(item => {
|
||||||
return {
|
return {
|
||||||
lang: 'de',
|
lang: 'de',
|
||||||
site_id: item.contentId,
|
site_id: item.contentId,
|
||||||
name: item.name
|
name: item.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategory(item) {
|
function parseCategory(item) {
|
||||||
return item.genres
|
return item.genres
|
||||||
? item.genres
|
? item.genres
|
||||||
.replace('und', ',')
|
.replace('und', ',')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(i => i.trim())
|
.map(i => i.trim())
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDirectors(item) {
|
function parseDirectors(item) {
|
||||||
if (!item.cast || !item.cast.director) return []
|
if (!item.cast || !item.cast.director) return []
|
||||||
return item.cast.director
|
return item.cast.director
|
||||||
.replace('und', ',')
|
.replace('und', ',')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(i => i.trim())
|
.map(i => i.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseProducers(item) {
|
function parseProducers(item) {
|
||||||
if (!item.cast || !item.cast.producer) return []
|
if (!item.cast || !item.cast.producer) return []
|
||||||
return item.cast.producer
|
return item.cast.producer
|
||||||
.replace('und', ',')
|
.replace('und', ',')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(i => i.trim())
|
.map(i => i.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAdapters(item) {
|
function parseAdapters(item) {
|
||||||
if (!item.cast || !item.cast.adaptor) return []
|
if (!item.cast || !item.cast.adaptor) return []
|
||||||
return item.cast.adaptor
|
return item.cast.adaptor
|
||||||
.replace('und', ',')
|
.replace('und', ',')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(i => i.trim())
|
.map(i => i.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseUrls(item) {
|
function parseUrls(item) {
|
||||||
// currently only a imdb id is returned by the api, thus we can construct the url here
|
// currently only a imdb id is returned by the api, thus we can construct the url here
|
||||||
if (!item.externalIds) return []
|
if (!item.externalIds) return []
|
||||||
return JSON.parse(item.externalIds)
|
return JSON.parse(item.externalIds)
|
||||||
.filter(externalId => externalId.type === 'imdb' && externalId.id)
|
.filter(externalId => externalId.type === 'imdb' && externalId.id)
|
||||||
.map(externalId => ({ system: 'imdb', value: `https://www.imdb.com/title/${externalId.id}` }))
|
.map(externalId => ({ system: 'imdb', value: `https://www.imdb.com/title/${externalId.id}` }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseImage(item) {
|
function parseImage(item) {
|
||||||
if (!Array.isArray(item.pictures) || !item.pictures.length) return null
|
if (!Array.isArray(item.pictures) || !item.pictures.length) return null
|
||||||
|
|
||||||
return item.pictures[0].href
|
return item.pictures[0].href
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart(item) {
|
function parseStart(item) {
|
||||||
return dayjs.utc(item.starttime, 'YYYY-MM-DD HH:mm:ss')
|
return dayjs.utc(item.starttime, 'YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStop(item) {
|
function parseStop(item) {
|
||||||
return dayjs.utc(item.endtime, 'YYYY-MM-DD HH:mm:ss')
|
return dayjs.utc(item.endtime, 'YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
if (!data || !Array.isArray(data.playbilllist)) return []
|
if (!data || !Array.isArray(data.playbilllist)) return []
|
||||||
|
|
||||||
return data.playbilllist
|
return data.playbilllist
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCookieAndToken() {
|
async function fetchCookieAndToken() {
|
||||||
// Only fetch the cookies and csrfToken if they are not already set
|
// Only fetch the cookies and csrfToken if they are not already set
|
||||||
if (X_CSRFTOKEN && Cookie) {
|
if (X_CSRFTOKEN && Cookie) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.request({
|
const response = await axios.request({
|
||||||
url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate',
|
url: 'https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate',
|
||||||
params: {
|
params: {
|
||||||
SID: 'firstup',
|
SID: 'firstup',
|
||||||
T: 'Windows_chrome_118'
|
T: 'Windows_chrome_118'
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: '{"terminalid":"00:00:00:00:00:00","mac":"00:00:00:00:00:00","terminaltype":"WEBTV","utcEnable":1,"timezone":"Etc/GMT0","userType":3,"terminalvendor":"Unknown"}',
|
data: '{"terminalid":"00:00:00:00:00:00","mac":"00:00:00:00:00:00","terminaltype":"WEBTV","utcEnable":1,"timezone":"Etc/GMT0","userType":3,"terminalvendor":"Unknown"}',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Extract the cookies specified in cookiesToExtract
|
// Extract the cookies specified in cookiesToExtract
|
||||||
const setCookieHeader = response.headers['set-cookie'] || []
|
const setCookieHeader = response.headers['set-cookie'] || []
|
||||||
const extractedCookies = []
|
const extractedCookies = []
|
||||||
cookiesToExtract.forEach(cookieName => {
|
cookiesToExtract.forEach(cookieName => {
|
||||||
const regex = new RegExp(`${cookieName}=(.+?)(;|$)`)
|
const regex = new RegExp(`${cookieName}=(.+?)(;|$)`)
|
||||||
const match = setCookieHeader.find(header => regex.test(header))
|
const match = setCookieHeader.find(header => regex.test(header))
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const cookieString = regex.exec(match)[0]
|
const cookieString = regex.exec(match)[0]
|
||||||
extractedCookies.push(cookieString)
|
extractedCookies.push(cookieString)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// check if we recieved a csrfToken only then store the values
|
// check if we recieved a csrfToken only then store the values
|
||||||
if (!response.data.csrfToken) {
|
if (!response.data.csrfToken) {
|
||||||
console.log('csrfToken not found in the response.')
|
console.log('csrfToken not found in the response.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
X_CSRFTOKEN = response.data.csrfToken
|
X_CSRFTOKEN = response.data.csrfToken
|
||||||
Cookie = extractedCookies.join(' ')
|
Cookie = extractedCookies.join(' ')
|
||||||
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setHeaders() {
|
async function setHeaders() {
|
||||||
await fetchCookieAndToken()
|
await fetchCookieAndToken()
|
||||||
|
|
||||||
return { X_CSRFTOKEN, Cookie }
|
return { X_CSRFTOKEN, Cookie }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,83 @@
|
|||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
type ExecError = {
|
interface ExecError {
|
||||||
status: number
|
status: number
|
||||||
stdout: string
|
stdout: string
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('channels:lint', () => {
|
describe('channels:lint', () => {
|
||||||
it('will show a message if the file contains a syntax error', () => {
|
it('will show a message if the file contains a syntax error', () => {
|
||||||
try {
|
try {
|
||||||
const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml'
|
const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml'
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect((error as ExecError).status).toBe(1)
|
expect((error as ExecError).status).toBe(1)
|
||||||
expect((error as ExecError).stdout).toContain(
|
expect((error as ExecError).stdout).toContain(
|
||||||
"error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n"
|
"error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n\n1 error(s)\n"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('will show a message if an error occurred while parsing an xml file', () => {
|
it('will show a message if an error occurred while parsing an xml file', () => {
|
||||||
try {
|
try {
|
||||||
const cmd =
|
const cmd =
|
||||||
'npm run channels:lint --- tests/__data__/input/channels_lint/invalid.channels.xml'
|
'npm run channels:lint --- tests/__data__/input/channels_lint/invalid.channels.xml'
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect((error as ExecError).status).toBe(1)
|
expect((error as ExecError).status).toBe(1)
|
||||||
expect((error as ExecError).stdout).toContain(
|
expect((error as ExecError).stdout).toContain(
|
||||||
'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n'
|
'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can test multiple files at ones', () => {
|
it('can test multiple files at ones', () => {
|
||||||
try {
|
try {
|
||||||
const cmd =
|
const cmd =
|
||||||
'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml tests/__data__/input/channels_lint/invalid.channels.xml'
|
'npm run channels:lint --- tests/__data__/input/channels_lint/error.channels.xml tests/__data__/input/channels_lint/invalid.channels.xml'
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect((error as ExecError).status).toBe(1)
|
expect((error as ExecError).status).toBe(1)
|
||||||
expect((error as ExecError).stdout).toContain(
|
expect((error as ExecError).stdout).toContain(
|
||||||
"error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n"
|
"error.channels.xml\n 3:0 Element 'channel': The attribute 'lang' is required but missing.\n"
|
||||||
)
|
)
|
||||||
expect((error as ExecError).stdout).toContain(
|
expect((error as ExecError).stdout).toContain(
|
||||||
'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n'
|
'invalid.channels.xml\n 2:6 XML declaration allowed only at the start of the document\n'
|
||||||
)
|
)
|
||||||
expect((error as ExecError).stdout).toContain('2 error(s)')
|
expect((error as ExecError).stdout).toContain('2 error(s)')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('will show a message if the file contains single quotes', () => {
|
it('will show a message if the file contains single quotes', () => {
|
||||||
try {
|
try {
|
||||||
const cmd =
|
const cmd =
|
||||||
'npm run channels:lint --- tests/__data__/input/channels_lint/single_quotes.channels.xml'
|
'npm run channels:lint --- tests/__data__/input/channels_lint/single_quotes.channels.xml'
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect((error as ExecError).status).toBe(1)
|
expect((error as ExecError).status).toBe(1)
|
||||||
expect((error as ExecError).stdout).toContain('single_quotes.channels.xml')
|
expect((error as ExecError).stdout).toContain('single_quotes.channels.xml')
|
||||||
expect((error as ExecError).stdout).toContain(
|
expect((error as ExecError).stdout).toContain(
|
||||||
'1:14 Single quotes cannot be used in attributes'
|
'1:14 Single quotes cannot be used in attributes'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not display errors if there are none', () => {
|
it('does not display errors if there are none', () => {
|
||||||
try {
|
try {
|
||||||
const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/valid.channels.xml'
|
const cmd = 'npm run channels:lint --- tests/__data__/input/channels_lint/valid.channels.xml'
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (process.env.DEBUG === 'true') console.log((error as ExecError).stdout)
|
if (process.env.DEBUG === 'true') console.log((error as ExecError).stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,70 +1,70 @@
|
|||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
type ExecError = {
|
interface ExecError {
|
||||||
status: number
|
status: number
|
||||||
stdout: string
|
stdout: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/__data__'
|
const ENV_VAR = 'cross-env DATA_DIR=tests/__data__/input/__data__'
|
||||||
|
|
||||||
describe('channels:validate', () => {
|
describe('channels:validate', () => {
|
||||||
it('will show a message if the file contains a duplicate', () => {
|
it('will show a message if the file contains a duplicate', () => {
|
||||||
try {
|
try {
|
||||||
const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/duplicate.channels.xml`
|
const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/duplicate.channels.xml`
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect((error as ExecError).status).toBe(1)
|
expect((error as ExecError).status).toBe(1)
|
||||||
expect((error as ExecError).stdout).toContain(`
|
expect((error as ExecError).stdout).toContain(`
|
||||||
┌─────────┬─────────────┬──────┬─────────────────┬─────────┬─────────┐
|
┌─────────┬─────────────┬──────┬─────────────────┬─────────┬─────────┐
|
||||||
│ (index) │ type │ lang │ xmltv_id │ site_id │ name │
|
│ (index) │ type │ lang │ xmltv_id │ site_id │ name │
|
||||||
├─────────┼─────────────┼──────┼─────────────────┼─────────┼─────────┤
|
├─────────┼─────────────┼──────┼─────────────────┼─────────┼─────────┤
|
||||||
│ 0 │ 'duplicate' │ 'en' │ 'Bravo.us@East' │ '140' │ 'Bravo' │
|
│ 0 │ 'duplicate' │ 'en' │ 'Bravo.us@East' │ '140' │ 'Bravo' │
|
||||||
└─────────┴─────────────┴──────┴─────────────────┴─────────┴─────────┘
|
└─────────┴─────────────┴──────┴─────────────────┴─────────┴─────────┘
|
||||||
|
|
||||||
1 error(s) in 1 file(s)
|
1 error(s) in 1 file(s)
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('will show a message if the file contains a channel with wrong channel id', () => {
|
it('will show a message if the file contains a channel with wrong channel id', () => {
|
||||||
try {
|
try {
|
||||||
const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_channel_id.channels.xml`
|
const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_channel_id.channels.xml`
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect((error as ExecError).status).toBe(1)
|
expect((error as ExecError).status).toBe(1)
|
||||||
expect((error as ExecError).stdout).toContain(`
|
expect((error as ExecError).stdout).toContain(`
|
||||||
┌─────────┬────────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐
|
┌─────────┬────────────────────┬──────┬────────────────────┬─────────┬─────────────────────┐
|
||||||
│ (index) │ type │ lang │ xmltv_id │ site_id │ name │
|
│ (index) │ type │ lang │ xmltv_id │ site_id │ name │
|
||||||
├─────────┼────────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤
|
├─────────┼────────────────────┼──────┼────────────────────┼─────────┼─────────────────────┤
|
||||||
│ 0 │ 'wrong_channel_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │
|
│ 0 │ 'wrong_channel_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │
|
||||||
└─────────┴────────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘
|
└─────────┴────────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘
|
||||||
|
|
||||||
1 error(s) in 1 file(s)
|
1 error(s) in 1 file(s)
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('will show a message if the file contains a channel with wrong feed id', () => {
|
it('will show a message if the file contains a channel with wrong feed id', () => {
|
||||||
try {
|
try {
|
||||||
const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_feed_id.channels.xml`
|
const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_feed_id.channels.xml`
|
||||||
const stdout = execSync(cmd, { encoding: 'utf8' })
|
const stdout = execSync(cmd, { encoding: 'utf8' })
|
||||||
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect((error as ExecError).status).toBe(1)
|
expect((error as ExecError).status).toBe(1)
|
||||||
expect((error as ExecError).stdout).toContain(`
|
expect((error as ExecError).stdout).toContain(`
|
||||||
┌─────────┬─────────────────┬──────┬─────────────────┬─────────┬─────────┐
|
┌─────────┬─────────────────┬──────┬─────────────────┬─────────┬─────────┐
|
||||||
│ (index) │ type │ lang │ xmltv_id │ site_id │ name │
|
│ (index) │ type │ lang │ xmltv_id │ site_id │ name │
|
||||||
├─────────┼─────────────────┼──────┼─────────────────┼─────────┼─────────┤
|
├─────────┼─────────────────┼──────┼─────────────────┼─────────┼─────────┤
|
||||||
│ 0 │ 'wrong_feed_id' │ 'en' │ 'Bravo.us@West' │ '150' │ 'Bravo' │
|
│ 0 │ 'wrong_feed_id' │ 'en' │ 'Bravo.us@West' │ '150' │ 'Bravo' │
|
||||||
└─────────┴─────────────────┴──────┴─────────────────┴─────────┴─────────┘
|
└─────────┴─────────────────┴──────┴─────────────────┴─────────┴─────────┘
|
||||||
|
|
||||||
1 error(s) in 1 file(s)
|
1 error(s) in 1 file(s)
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user