Merge pull request #2814 from iptv-org/patch-2025.07.1

Patch 2025.07.1
This commit is contained in:
PopeyeTheSai10r
2025-07-26 09:10:40 -07:00
committed by GitHub
60 changed files with 2389 additions and 12518 deletions
+4 -3
View File
@@ -18,22 +18,23 @@ jobs:
- name: changed files - name: changed files
id: files id: files
run: | run: |
git fetch origin master:master
JS_ANY_CHANGED=false JS_ANY_CHANGED=false
JS_ALL_CHANGED_FILES=$(git diff --name-only tests/**/*.js tests/**/*.ts scripts/**/*.js scripts/**/*.mts scripts/**/*.ts sites/**/*.js sites/**/*.ts | tr '\n' ' ') JS_ALL_CHANGED_FILES=$(git diff --name-only master -- tests/**/*.js tests/**/*.ts scripts/**/*.js scripts/**/*.mts scripts/**/*.ts sites/**/*.js sites/**/*.ts | tr '\n' ' ')
if [ -n "${JS_ALL_CHANGED_FILES}" ]; then if [ -n "${JS_ALL_CHANGED_FILES}" ]; then
JS_ANY_CHANGED=true JS_ANY_CHANGED=true
fi fi
echo "js_all_changed_files=$JS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT" echo "js_all_changed_files=$JS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT"
echo "js_any_changed=$JS_ANY_CHANGED" >> "$GITHUB_OUTPUT" echo "js_any_changed=$JS_ANY_CHANGED" >> "$GITHUB_OUTPUT"
CHANNELS_ANY_CHANGED=false CHANNELS_ANY_CHANGED=false
CHANNELS_ALL_CHANGED_FILES=$(git diff --name-only sites/**/*.channels.xml | tr '\n' ' ') CHANNELS_ALL_CHANGED_FILES=$(git diff --name-only master -- sites/**/*.channels.xml | tr '\n' ' ')
if [ -n "${CHANNELS_ALL_CHANGED_FILES}" ]; then if [ -n "${CHANNELS_ALL_CHANGED_FILES}" ]; then
CHANNELS_ANY_CHANGED=true CHANNELS_ANY_CHANGED=true
fi fi
echo "channels_all_changed_files=$CHANNELS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT" echo "channels_all_changed_files=$CHANNELS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT"
echo "channels_any_changed=$CHANNELS_ANY_CHANGED" >> "$GITHUB_OUTPUT" echo "channels_any_changed=$CHANNELS_ANY_CHANGED" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
if: ${{ !env.ACT && (steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true') }} if: ${{ steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true' }}
with: with:
node-version: 22 node-version: 22
cache: 'npm' cache: 'npm'
+3 -4
View File
@@ -7,14 +7,14 @@ jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: tibdex/github-app-token@v1.8.2 - uses: tibdex/github-app-token@v1.8.2
if: ${{ !env.ACT }} if: ${{ !env.ACT }}
id: create-app-token id: create-app-token
with: with:
app_id: ${{ secrets.APP_ID }} app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }} private_key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v3 - uses: actions/checkout@v4
if: ${{ !env.ACT }} if: ${{ !env.ACT }}
with: with:
token: ${{ steps.create-app-token.outputs.token }} token: ${{ steps.create-app-token.outputs.token }}
@@ -22,8 +22,7 @@ jobs:
run: | run: |
git config user.name "iptv-bot[bot]" git config user.name "iptv-bot[bot]"
git config user.email "84861620+iptv-bot[bot]@users.noreply.github.com" git config user.email "84861620+iptv-bot[bot]@users.noreply.github.com"
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
if: ${{ !env.ACT }}
with: with:
node-version: 22 node-version: 22
cache: 'npm' cache: 'npm'
+8 -9
View File
@@ -65,6 +65,7 @@ Options:
--days <days> Number of days for which the program will be loaded (defaults to the value from the site config) --days <days> Number of days for which the program will be loaded (defaults to the value from the site config)
--maxConnections <number> Number of concurrent requests (default: 1) --maxConnections <number> Number of concurrent requests (default: 1)
--gzip Specifies whether or not to create a compressed version of the guide (default: false) --gzip Specifies whether or not to create a compressed version of the guide (default: false)
--curl Display each request as CURL (default: false)
``` ```
### Parallel downloading ### Parallel downloading
@@ -97,19 +98,15 @@ npm run grab --- --channels=path/to/custom.channels.xml
### Run on schedule ### Run on schedule
To download the guide on a schedule, you can use the included process manager. Just run it with desire [cron expression](https://crontab.guru/) and the `grab` options: If you want to download guides on a schedule, you can use [cron](https://en.wikipedia.org/wiki/Cron) or any other task scheduler. Currently, we use a tool called `chronos` for this purpose.
To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/):
```sh ```sh
npx pm2 start npm --no-autorestart --cron-restart="0 0,12 * * *" -- run grab --- --site=example.com npx chronos --execute="npm run grab --- --site=example.com" --pattern="0 0,12 * * *" --log
``` ```
To track the process, you can use the command: For more info go to [chronos](https://github.com/freearhey/chronos) documentation.
```sh
npx pm2 logs
```
For more info go to [pm2](https://pm2.keymetrics.io/docs/usage/quick-start/) documentation.
### Access the guide by URL ### Access the guide by URL
@@ -186,6 +183,7 @@ docker run \
-e CRON_SCHEDULE="0 0,12 * * *" \ -e CRON_SCHEDULE="0 0,12 * * *" \
-e MAX_CONNECTIONS=10 \ -e MAX_CONNECTIONS=10 \
-e GZIP=true \ -e GZIP=true \
-e CURL=true \
-e PROXY="socks5://127.0.0.1:1234" \ -e PROXY="socks5://127.0.0.1:1234" \
-e DAYS=14 \ -e DAYS=14 \
-e TIMEOUT=5 \ -e TIMEOUT=5 \
@@ -198,6 +196,7 @@ iptv-org/epg
| CRON_SCHEDULE | A [cron expression](https://crontab.guru/) describing the schedule of the guide loadings (default: "0 0 \* \* \*") | | CRON_SCHEDULE | A [cron expression](https://crontab.guru/) describing the schedule of the guide loadings (default: "0 0 \* \* \*") |
| MAX_CONNECTIONS | Limit on the number of concurrent requests (default: 1) | | MAX_CONNECTIONS | Limit on the number of concurrent requests (default: 1) |
| GZIP | Boolean value indicating whether to create a compressed version of the guide (default: false) | | GZIP | Boolean value indicating whether to create a compressed version of the guide (default: false) |
| CURL | Display each request as CURL (default: false) |
| PROXY | Use the specified proxy | | PROXY | Use the specified proxy |
| DAYS | Number of days for which the guide will be loaded (defaults to the value from the site config) | | DAYS | Number of days for which the guide will be loaded (defaults to the value from the site config) |
| TIMEOUT | Timeout for each request in milliseconds (default: 0) | | TIMEOUT | Timeout for each request in milliseconds (default: 0) |
+1536 -3517
View File
File diff suppressed because it is too large Load Diff
+14 -15
View File
@@ -18,7 +18,6 @@
"prepare": "husky" "prepare": "husky"
}, },
"private": true, "private": true,
"author": "Arhey",
"license": "UNLICENSED", "license": "UNLICENSED",
"jest": { "jest": {
"setupFiles": [ "setupFiles": [
@@ -40,27 +39,28 @@
"@alex_neo/jest-expect-message": "^1.0.5", "@alex_neo/jest-expect-message": "^1.0.5",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.30.0", "@eslint/js": "^9.30.0",
"@freearhey/core": "^0.8.2", "@freearhey/chronos": "^0.0.1",
"@freearhey/core": "^0.10.2",
"@freearhey/search-js": "^0.1.2", "@freearhey/search-js": "^0.1.2",
"@ntlab/sfetch": "^1.2.0", "@ntlab/sfetch": "^1.2.0",
"@octokit/core": "^7.0.2", "@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-rest": "^13.1.1", "@octokit/plugin-paginate-rest": "^13.1.1",
"@octokit/plugin-rest-endpoint-methods": "^16.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0",
"@swc/core": "^1.12.7", "@swc/core": "^1.13.0",
"@swc/jest": "^0.2.38", "@swc/jest": "^0.2.39",
"@types/cli-progress": "^3.11.6", "@types/cli-progress": "^3.11.6",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/inquirer": "^9.0.8", "@types/inquirer": "^9.0.8",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/langs": "^2.0.5", "@types/langs": "^2.0.5",
"@types/lodash": "^4.17.19", "@types/lodash": "^4.17.19",
"@types/node": "^24.0.7", "@types/node": "^24.0.14",
"@types/node-cleanup": "^2.1.5", "@types/node-cleanup": "^2.1.5",
"@types/numeral": "^2.0.5", "@types/numeral": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^8.35.0", "@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/parser": "^8.35.0", "@typescript-eslint/parser": "^8.35.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"axios-cookiejar-support": "^6.0.2", "axios-cookiejar-support": "^6.0.4",
"chalk": "^5.4.1", "chalk": "^5.4.1",
"cheerio": "^1.1.0", "cheerio": "^1.1.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
@@ -70,23 +70,23 @@
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"cwait": "^1.1.2", "cwait": "^1.1.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"epg-grabber": "^0.38.0", "epg-grabber": "^0.41.0",
"epg-parser": "^0.3.1", "epg-parser": "^0.3.1",
"eslint": "^9.30.0", "eslint": "^9.30.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.8",
"form-data": "^4.0.3", "form-data": "^4.0.4",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"globals": "^16.2.0", "globals": "^16.3.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"inquirer": "^12.6.3", "inquirer": "^12.7.0",
"jest": "^30.0.3", "jest": "^30.0.3",
"jest-offline": "^1.0.1", "jest-offline": "^1.0.1",
"langs": "^2.0.0", "langs": "^2.0.0",
"libxml2-wasm": "^0.5.0", "libxml2-wasm": "^0.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"luxon": "^3.6.1", "luxon": "^3.7.1",
"mockdate": "^3.0.5", "mockdate": "^3.0.5",
"nedb-promises": "^6.2.3", "nedb-promises": "^6.2.3",
"node-cleanup": "^2.1.2", "node-cleanup": "^2.1.2",
@@ -111,6 +111,5 @@
"typescript": "^5.8.3", "typescript": "^5.8.3",
"unzipit": "^1.4.3", "unzipit": "^1.4.3",
"wildcard-match": "^5.1.4" "wildcard-match": "^5.1.4"
}, }
"packageManager": "yarn@4.9.2"
} }
+8 -7
View File
@@ -1,3 +1,9 @@
const grab = process.env.SITE
? `npm run grab -- --site=${process.env.SITE} ${
process.env.CLANG ? `--lang=${process.env.CLANG}` : ''
} --output=public/guide.xml`
: 'npm run grab -- --channels=channels.xml --output=public/guide.xml'
module.exports = { module.exports = {
apps: [ apps: [
{ {
@@ -9,15 +15,10 @@ module.exports = {
}, },
{ {
name: 'grab', name: 'grab',
script: process.env.SITE script: `npx chronos -e "${grab}" -p "${process.env.CRON_SCHEDULE}" -l`,
? `npm run grab -- --site=${process.env.SITE} ${
process.env.CLANG ? `--lang=${process.env.CLANG}` : ''
} --output=public/guide.xml`
: 'npm run grab -- --channels=channels.xml --output=public/guide.xml',
cron_restart: process.env.CRON_SCHEDULE || null,
instances: 1, instances: 1,
watch: false, watch: false,
autorestart: false autorestart: true
} }
] ]
} }
+16 -31
View File
@@ -1,17 +1,9 @@
import { Logger, Storage, Collection } from '@freearhey/core' import { Logger, Collection, Storage } from '@freearhey/core'
import { ChannelsParser } from '../../core'
import path from 'path'
import { SITES_DIR, API_DIR } from '../../constants' import { SITES_DIR, API_DIR } from '../../constants'
import { GuideChannel } from '../../models'
import { ChannelsParser } from '../../core'
import epgGrabber from 'epg-grabber' import epgGrabber from 'epg-grabber'
import path from 'path'
type OutputItem = {
channel: string | null
feed: string | null
site: string
site_id: string
site_name: string
lang: string
}
async function main() { async function main() {
const logger = new Logger() const logger = new Logger()
@@ -20,31 +12,24 @@ async function main() {
logger.info('loading channels...') logger.info('loading channels...')
const sitesStorage = new Storage(SITES_DIR) const sitesStorage = new Storage(SITES_DIR)
const parser = new ChannelsParser({ storage: sitesStorage }) const parser = new ChannelsParser({
storage: sitesStorage
})
let files: string[] = [] const files: string[] = await sitesStorage.list('**/*.channels.xml')
files = await sitesStorage.list('**/*.channels.xml')
let parsedChannels = new Collection() const channels = new Collection()
for (const filepath of files) { for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath)) const channelList = await parser.parse(filepath)
channelList.channels.forEach((data: epgGrabber.Channel) => {
channels.add(new GuideChannel(data))
})
} }
logger.info(` found ${parsedChannels.count()} channel(s)`) logger.info(`found ${channels.count()} channel(s)`)
const output = parsedChannels.map((channel: epgGrabber.Channel): OutputItem => { const output = channels.map((channel: GuideChannel) => channel.toJSON())
const xmltv_id = channel.xmltv_id || ''
const [channelId, feedId] = xmltv_id.split('@')
return {
channel: channelId || null,
feed: feedId || null,
site: channel.site || '',
site_id: channel.site_id || '',
site_name: channel.name,
lang: channel.lang || ''
}
})
const apiStorage = new Storage(API_DIR) const apiStorage = new Storage(API_DIR)
const outputFilename = 'guides.json' const outputFilename = 'guides.json'
+2 -1
View File
@@ -17,7 +17,8 @@ async function main() {
loader.download('feeds.json'), loader.download('feeds.json'),
loader.download('timezones.json'), loader.download('timezones.json'),
loader.download('guides.json'), loader.download('guides.json'),
loader.download('streams.json') loader.download('streams.json'),
loader.download('logos.json')
]) ])
} }
+22 -23
View File
@@ -1,17 +1,17 @@
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core' import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
import type { DataProcessorData } from '../../types/dataProcessor'
import type { DataLoaderData } from '../../types/dataLoader'
import { ChannelSearchableData } from '../../types/channel'
import { Channel, ChannelList, Feed } from '../../models'
import { DataProcessor, DataLoader } from '../../core'
import { select, input } from '@inquirer/prompts' import { select, input } from '@inquirer/prompts'
import { ChannelsParser, XML } from '../../core' import { ChannelsParser } from '../../core'
import { Channel, Feed } from '../../models'
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 epgGrabber from 'epg-grabber'
import { Command } from 'commander' import { Command } from 'commander'
import readline from 'readline' import readline from 'readline'
import sjs from '@freearhey/search-js'
import { DataProcessor, DataLoader } from '../../core'
import type { DataLoaderData } from '../../types/dataLoader'
import type { DataProcessorData } from '../../types/dataProcessor'
import epgGrabber from 'epg-grabber'
import { ChannelSearchableData } from '../../types/channel'
type ChoiceValue = { type: string; value?: Feed | Channel } type ChoiceValue = { type: string; value?: Feed | Channel }
type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean } type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean }
@@ -34,11 +34,11 @@ program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(proc
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 parsedChannels = new Collection() let channelList = new ChannelList({ channels: [] })
main(filepath) main(filepath)
nodeCleanup(() => { nodeCleanup(() => {
save(filepath) save(filepath, channelList)
}) })
export default async function main(filepath: string) { export default async function main(filepath: string) {
@@ -51,18 +51,18 @@ export default async function main(filepath: string) {
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 { feedsGroupedByChannelId, channels, channelsKeyById }: 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 })
parsedChannels = await parser.parse(filepath) channelList = await parser.parse(filepath)
const parsedChannelsWithoutId = parsedChannels.filter( const parsedChannelsWithoutId = channelList.channels.filter(
(channel: epgGrabber.Channel) => !channel.xmltv_id (channel: epgGrabber.Channel) => !channel.xmltv_id
) )
logger.info( logger.info(
`found ${parsedChannels.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...')
@@ -73,10 +73,10 @@ export default async function main(filepath: string) {
logger.info('starting...\n') logger.info('starting...\n')
for (const parsedChannel of parsedChannelsWithoutId.all()) { for (const channel of parsedChannelsWithoutId.all()) {
try { try {
parsedChannel.xmltv_id = await selectChannel( channel.xmltv_id = await selectChannel(
parsedChannel, channel,
searchIndex, searchIndex,
feedsGroupedByChannelId, feedsGroupedByChannelId,
channelsKeyById channelsKeyById
@@ -124,8 +124,8 @@ async function selectChannel(
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('@')
} }
} }
@@ -153,7 +153,7 @@ async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary
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 ''
@@ -205,10 +205,9 @@ function getFeedChoises(feeds: Collection): Choice[] {
return choises return choises
} }
function save(filepath: string) { function save(filepath: string, channelList: ChannelList) {
if (!storage.existsSync(filepath)) return if (!storage.existsSync(filepath)) return
const xml = new XML(parsedChannels) storage.saveSync(filepath, channelList.toString())
storage.saveSync(filepath, xml.toString())
logger.info(`\nFile '${filepath}' successfully saved`) logger.info(`\nFile '${filepath}' successfully saved`)
} }
+1
View File
@@ -19,6 +19,7 @@ const xsd = `<?xml version="1.0" encoding="UTF-8"?>
<xs:attribute use="required" ref="site_id"/> <xs:attribute use="required" ref="site_id"/>
<xs:attribute name="xmltv_id" use="required" type="xs:string"/> <xs:attribute name="xmltv_id" use="required" type="xs:string"/>
<xs:attribute name="logo" type="xs:string"/> <xs:attribute name="logo" type="xs:string"/>
<xs:attribute name="lcn" type="xs:string"/>
</xs:complexType> </xs:complexType>
</xs:element> </xs:element>
<xs:attribute name="site"> <xs:attribute name="site">
+25 -30
View File
@@ -1,8 +1,9 @@
import { Logger, File, Collection, Storage } from '@freearhey/core' import { Logger, File, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core' import { ChannelsParser } from '../../core'
import { Channel } from 'epg-grabber' import { ChannelList } from '../../models'
import { Command } from 'commander'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import epgGrabber from 'epg-grabber'
import { Command } from 'commander'
const program = new Command() const program = new Command()
program program
@@ -21,17 +22,25 @@ type ParseOptions = {
const options: ParseOptions = program.opts() const options: ParseOptions = program.opts()
async function main() { async function main() {
function isPromise(promise: object[] | Promise<object[]>) {
return (
!!promise &&
typeof promise === 'object' &&
typeof (promise as Promise<object[]>).then === 'function'
)
}
const storage = new Storage() const storage = new Storage()
const parser = new ChannelsParser({ storage })
const logger = new Logger() const logger = new Logger()
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 channels = new Collection() let channelList = new ChannelList({ channels: [] })
if (await storage.exists(outputFilepath)) { if (await storage.exists(outputFilepath)) {
channels = await parser.parse(outputFilepath) channelList = await parser.parse(outputFilepath)
} }
const args: { const args: {
@@ -49,45 +58,31 @@ async function main() {
if (isPromise(parsedChannels)) { if (isPromise(parsedChannels)) {
parsedChannels = await parsedChannels parsedChannels = await parsedChannels
} }
parsedChannels = parsedChannels.map((channel: Channel) => { parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
channel.site = config.site channel.site = config.site
return channel return channel
}) })
let output = new Collection() const newChannelList = new ChannelList({ channels: [] })
parsedChannels.forEach((channel: Channel) => { parsedChannels.forEach((channel: epgGrabber.Channel) => {
const found: Channel | undefined = channels.first( if (!channel.site_id) return
(_channel: Channel) => _channel.site_id == channel.site_id
) const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
if (found) { if (found) {
channel.xmltv_id = found.xmltv_id channel.xmltv_id = found.xmltv_id
channel.lang = found.lang channel.lang = found.lang
} }
output.add(channel) newChannelList.add(channel)
}) })
output = output.orderBy([ newChannelList.sort()
(channel: Channel) => channel.lang || '_',
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
(channel: Channel) => channel.site_id
])
const xml = new XML(output) await storage.save(outputFilepath, newChannelList.toString())
await storage.save(outputFilepath, xml.toString())
logger.info(`File '${outputFilepath}' successfully saved`) logger.info(`File '${outputFilepath}' successfully saved`)
} }
main() main()
function isPromise(promise: object[] | Promise<object[]>) {
return (
!!promise &&
typeof promise === 'object' &&
typeof (promise as Promise<object[]>).then === 'function'
)
}
+29 -16
View File
@@ -1,16 +1,18 @@
import { Storage, Collection, Dictionary, File } from '@freearhey/core' import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
import { ChannelsParser } from '../../core' import { DataProcessorData } from '../../types/dataProcessor'
import { Channel } from '../../models' import { Storage, Dictionary, File } from '@freearhey/core'
import { DataLoaderData } from '../../types/dataLoader'
import { ChannelList } from '../../models'
import { DATA_DIR } from '../../constants'
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'
import { DATA_DIR } from '../../constants'
import epgGrabber from 'epg-grabber'
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 = { type ValidationError = {
type: 'duplicate' | 'wrong_xmltv_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
@@ -19,12 +21,14 @@ type ValidationError = {
} }
async function main() { async function main() {
const parser = new ChannelsParser({ storage: new Storage() }) const processor = new DataProcessor()
const dataStorage = new Storage(DATA_DIR) const dataStorage = new Storage(DATA_DIR)
const channelsData = await dataStorage.json('channels.json') const loader = new DataLoader({ storage: dataStorage })
const channels = new Collection(channelsData).map(data => new Channel(data)) const data: DataLoaderData = await loader.load()
const channelsGroupedById = channels.groupBy((channel: Channel) => channel.id) const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
const parser = new ChannelsParser({
storage: new Storage()
})
let totalFiles = 0 let totalFiles = 0
let totalErrors = 0 let totalErrors = 0
@@ -35,11 +39,11 @@ async function main() {
const file = new File(filepath) const file = new File(filepath)
if (file.extension() !== 'xml') continue if (file.extension() !== 'xml') continue
const parsedChannels = await parser.parse(filepath) const channelList: ChannelList = await parser.parse(filepath)
const bufferBySiteId = new Dictionary() const bufferBySiteId = new Dictionary()
const errors: ValidationError[] = [] const errors: ValidationError[] = []
parsedChannels.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)
@@ -54,12 +58,21 @@ async function main() {
} }
if (!channel.xmltv_id) return if (!channel.xmltv_id) return
const [channelId] = channel.xmltv_id.split('@') const [channelId, feedId] = channel.xmltv_id.split('@')
const foundChannel = channelsGroupedById.get(channelId)
const foundChannel = channelsKeyById.get(channelId)
if (!foundChannel) { if (!foundChannel) {
errors.push({ type: 'wrong_xmltv_id', ...channel }) errors.push({ type: 'wrong_channel_id', ...channel })
totalErrors++ totalErrors++
} }
if (feedId) {
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
if (!foundFeed) {
errors.push({ type: 'wrong_feed_id', ...channel })
totalErrors++
}
}
}) })
if (errors.length) { if (errors.length) {
+16 -9
View File
@@ -1,9 +1,10 @@
import { Logger, Timer, Storage, Collection } from '@freearhey/core' import { Logger, Timer, Storage, Collection } from '@freearhey/core'
import { Option, program } from 'commander'
import { QueueCreator, Job, ChannelsParser } from '../../core' import { QueueCreator, Job, ChannelsParser } from '../../core'
import { Option, program } from 'commander'
import { SITES_DIR } from '../../constants'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
import path from 'path' import path from 'path'
import { SITES_DIR } from '../../constants' 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'))
@@ -44,6 +45,7 @@ program
.default(false) .default(false)
.env('GZIP') .env('GZIP')
) )
.addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL'))
.parse() .parse()
export type GrabOptions = { export type GrabOptions = {
@@ -51,6 +53,7 @@ export type GrabOptions = {
channels?: string channels?: string
output: string output: string
gzip: boolean gzip: boolean
curl: boolean
maxConnections: number maxConnections: number
timeout?: string timeout?: string
delay?: string delay?: string
@@ -85,31 +88,35 @@ async function main() {
files = await storage.list(options.channels) files = await storage.list(options.channels)
} }
let parsedChannels = new Collection() let channels = new Collection()
for (const filepath of files) { for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath)) const channelList: ChannelList = await parser.parse(filepath)
channels = channels.concat(channelList.channels)
} }
if (options.lang) { if (options.lang) {
parsedChannels = parsedChannels.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 ${parsedChannels.count()} channel(s)`)
logger.info(` found ${channels.count()} channel(s)`)
logger.info('run:') logger.info('run:')
runJob({ logger, parsedChannels }) runJob({ logger, channels })
} }
main() main()
async function runJob({ logger, parsedChannels }: { logger: Logger; parsedChannels: 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({
parsedChannels, channels,
logger, logger,
options options
}) })
+17 -11
View File
@@ -1,21 +1,25 @@
import { IssueLoader, HTMLTable, ChannelsParser } from '../../core' import { IssueLoader, HTMLTable, ChannelsParser } from '../../core'
import { Logger, Storage, Collection } from '@freearhey/core' import { Logger, Storage, Collection } from '@freearhey/core'
import { ChannelList, Issue, Site } from '../../models'
import { SITES_DIR, ROOT_DIR } from '../../constants' import { SITES_DIR, ROOT_DIR } from '../../constants'
import { Issue, Site } from '../../models'
import { Channel } from 'epg-grabber' import { Channel } from 'epg-grabber'
async function main() { async function main() {
const logger = new Logger({ disabled: true }) const logger = new Logger({ level: -999 })
const loader = new IssueLoader() const issueLoader = new IssueLoader()
const sitesStorage = new Storage(SITES_DIR) const sitesStorage = new Storage(SITES_DIR)
const channelsParser = new ChannelsParser({ storage: sitesStorage })
const sites = new Collection() const sites = new Collection()
logger.info('loading channels...')
const channelsParser = new ChannelsParser({
storage: sitesStorage
})
logger.info('loading list of sites') logger.info('loading list of sites')
const folders = await sitesStorage.list('*/') const folders = await sitesStorage.list('*/')
logger.info('loading issues...') logger.info('loading issues...')
const issues = await loader.load() const issues = await issueLoader.load()
logger.info('putting the data together...') logger.info('putting the data together...')
const brokenGuideReports = issues.filter(issue => const brokenGuideReports = issues.filter(issue =>
@@ -33,19 +37,21 @@ async function main() {
const files = await sitesStorage.list(`${domain}/*.channels.xml`) const files = await sitesStorage.list(`${domain}/*.channels.xml`)
for (const filepath of files) { for (const filepath of files) {
const channels = await channelsParser.parse(filepath) const channelList: ChannelList = await channelsParser.parse(filepath)
site.totalChannels += channels.count() site.totalChannels += channelList.channels.count()
site.markedChannels += channels.filter((channel: Channel) => channel.xmltv_id).count() site.markedChannels += channelList.channels
.filter((channel: Channel) => channel.xmltv_id)
.count()
} }
sites.add(site) sites.add(site)
} }
logger.info('creating sites table...') logger.info('creating sites table...')
const data = new Collection() const tableData = new Collection()
sites.forEach((site: Site) => { sites.forEach((site: Site) => {
data.add([ tableData.add([
{ value: `<a href="sites/${site.domain}">${site.domain}</a>` }, { value: `<a href="sites/${site.domain}">${site.domain}</a>` },
{ value: site.totalChannels, align: 'right' }, { value: site.totalChannels, align: 'right' },
{ value: site.markedChannels, align: 'right' }, { value: site.markedChannels, align: 'right' },
@@ -55,7 +61,7 @@ async function main() {
}) })
logger.info('updating sites.md...') logger.info('updating sites.md...')
const table = new HTMLTable(data.all(), [ const table = new HTMLTable(tableData.all(), [
{ name: 'Site', align: 'left' }, { name: 'Site', align: 'left' },
{ name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' }, { name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' },
{ name: 'Status', align: 'left' }, { name: 'Status', align: 'left' },
+5 -7
View File
@@ -1,5 +1,6 @@
import { parseChannels } from 'epg-grabber' import { parseChannels } from 'epg-grabber'
import { Storage, Collection } from '@freearhey/core' import { Storage } from '@freearhey/core'
import { ChannelList } from '../models'
type ChannelsParserProps = { type ChannelsParserProps = {
storage: Storage storage: Storage
@@ -12,13 +13,10 @@ export class ChannelsParser {
this.storage = storage this.storage = storage
} }
async parse(filepath: string) { async parse(filepath: string): Promise<ChannelList> {
let parsedChannels = new Collection()
const content = await this.storage.load(filepath) const content = await this.storage.load(filepath)
const channels = parseChannels(content) const parsed = parseChannels(content)
parsedChannels = parsedChannels.concat(new Collection(channels))
return parsedChannels return new ChannelList({ channels: parsed })
} }
} }
+6 -3
View File
@@ -49,7 +49,8 @@ export class DataLoader {
feeds, feeds,
timezones, timezones,
guides, guides,
streams streams,
logos
] = await Promise.all([ ] = await Promise.all([
this.storage.json('countries.json'), this.storage.json('countries.json'),
this.storage.json('regions.json'), this.storage.json('regions.json'),
@@ -61,7 +62,8 @@ export class DataLoader {
this.storage.json('feeds.json'), this.storage.json('feeds.json'),
this.storage.json('timezones.json'), this.storage.json('timezones.json'),
this.storage.json('guides.json'), this.storage.json('guides.json'),
this.storage.json('streams.json') this.storage.json('streams.json'),
this.storage.json('logos.json')
]) ])
return { return {
@@ -75,7 +77,8 @@ export class DataLoader {
feeds, feeds,
timezones, timezones,
guides, guides,
streams streams,
logos
} }
} }
+26 -9
View File
@@ -1,6 +1,6 @@
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'
import { Channel, Feed, Guide, Stream } from '../models'
export class DataProcessor { export class DataProcessor {
constructor() {} constructor() {}
@@ -9,31 +9,48 @@ export class DataProcessor {
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 guides = new Collection(data.guides).map(data => new Guide(data)) const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId()) const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
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())
const feeds = new Collection(data.feeds).map(data => let feeds = new Collection(data.feeds).map(data =>
new Feed(data) new Feed(data)
.withGuides(guidesGroupedByStreamId) .withGuideChannels(guideChannelsGroupedByStreamId)
.withStreams(streamsGroupedById) .withStreams(streamsGroupedById)
.withChannel(channelsKeyById) .withChannel(channelsKeyById)
) )
const feedsKeyByStreamId = feeds.keyBy((feed: Feed) => feed.getStreamId())
const logos = new Collection(data.logos).map(data =>
new Logo(data).withFeed(feedsKeyByStreamId)
)
const logosGroupedByChannelId = logos.groupBy((logo: Logo) => logo.channelId)
const logosGroupedByStreamId = logos.groupBy((logo: Logo) => logo.getStreamId())
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) => channel.withFeeds(feedsGroupedByChannelId)) channels = channels.map((channel: Channel) =>
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
)
return { return {
guideChannelsGroupedByStreamId,
feedsGroupedByChannelId, feedsGroupedByChannelId,
guidesGroupedByStreamId, logosGroupedByChannelId,
logosGroupedByStreamId,
streamsGroupedById, streamsGroupedById,
feedsKeyByStreamId,
channelsKeyById, channelsKeyById,
guideChannels,
channels, channels,
streams, streams,
guides, feeds,
feeds logos
} }
} }
} }
+4
View File
@@ -70,6 +70,10 @@ export class Grabber {
} }
} }
if (this.options.curl === true) {
config.curl = true
}
const _programs = await this.grabber.grab( const _programs = await this.grabber.grab(
channel, channel,
date, date,
-60
View File
@@ -1,60 +0,0 @@
import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core'
import { Channel } from 'epg-grabber'
import { XMLTV } from '../core'
import path from 'path'
type GuideProps = {
channels: Collection
programs: Collection
logger: Logger
filepath: string
gzip: boolean
}
export class Guide {
channels: Collection
programs: Collection
logger: Logger
storage: Storage
filepath: string
gzip: boolean
constructor({ channels, programs, logger, filepath, gzip }: GuideProps) {
this.channels = channels
this.programs = programs
this.logger = logger
this.storage = new Storage(path.dirname(filepath))
this.filepath = filepath
this.gzip = gzip || false
}
async save() {
const channels = this.channels.uniqBy(
(channel: Channel) => `${channel.xmltv_id}:${channel.site}`
)
const programs = this.programs
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
timezone: 'UTC'
})
const xmltv = new XMLTV({
channels,
programs,
date: currDate
})
const xmlFilepath = this.filepath
const xmlFilename = path.basename(xmlFilepath)
this.logger.info(` saving to "${xmlFilepath}"...`)
await this.storage.save(xmlFilename, xmltv.toString())
if (this.gzip) {
const zip = new Zip()
const compressed = zip.compress(xmltv.toString())
const gzFilepath = `${this.filepath}.gz`
const gzFilename = path.basename(gzFilepath)
this.logger.info(` saving to "${gzFilepath}"...`)
await this.storage.save(gzFilename, compressed)
}
}
}
+63 -13
View File
@@ -1,7 +1,12 @@
import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core' import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
import { OptionValues } from 'commander' import { OptionValues } from 'commander'
import { Channel, Program } from 'epg-grabber' import { Channel, Feed, Guide } from '../models'
import { Guide } from '.' import path from 'path'
import { DataLoader, DataProcessor } from '.'
import { DataLoaderData } from '../types/dataLoader'
import { DataProcessorData } from '../types/dataProcessor'
import { DATA_DIR } from '../constants'
type GuideManagerProps = { type GuideManagerProps = {
options: OptionValues options: OptionValues
@@ -12,7 +17,6 @@ type GuideManagerProps = {
export class GuideManager { export class GuideManager {
options: OptionValues options: OptionValues
storage: Storage
logger: Logger logger: Logger
channels: Collection channels: Collection
programs: Collection programs: Collection
@@ -22,22 +26,51 @@ export class GuideManager {
this.logger = logger this.logger = logger
this.channels = channels this.channels = channels
this.programs = programs this.programs = programs
this.storage = new Storage()
} }
async createGuides() { async createGuides() {
const pathTemplate = new StringTemplate(this.options.output) const pathTemplate = new StringTemplate(this.options.output)
const processor = new DataProcessor()
const dataStorage = new Storage(DATA_DIR)
const loader = new DataLoader({ storage: dataStorage })
const data: DataLoaderData = await loader.load()
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
const groupedChannels = this.channels const groupedChannels = this.channels
.orderBy([(channel: Channel) => channel.xmltv_id]) .map((channel: epgGrabber.Channel) => {
.uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`) if (channel.xmltv_id && !channel.icon) {
.groupBy((channel: Channel) => { const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
if (foundFeed && foundFeed.hasLogo()) {
channel.icon = foundFeed.getLogoUrl()
} else {
const [channelId] = channel.xmltv_id.split('@')
const foundChannel: Channel = channelsKeyById.get(channelId)
if (foundChannel && foundChannel.hasLogo()) {
channel.icon = foundChannel.getLogoUrl()
}
}
}
return channel
})
.orderBy([
(channel: epgGrabber.Channel) => channel.index,
(channel: epgGrabber.Channel) => channel.xmltv_id
])
.uniqBy(
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
)
.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([(program: Program) => program.channel, (program: Program) => program.start]) .orderBy([
.groupBy((program: Program) => { (program: epgGrabber.Program) => program.channel,
(program: epgGrabber.Program) => program.start
])
.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
@@ -51,11 +84,28 @@ export class GuideManager {
filepath: groupKey, filepath: groupKey,
gzip: this.options.gzip, gzip: this.options.gzip,
channels: new Collection(groupedChannels.get(groupKey)), channels: new Collection(groupedChannels.get(groupKey)),
programs: new Collection(groupedPrograms.get(groupKey)), programs: new Collection(groupedPrograms.get(groupKey))
logger: this.logger
}) })
await guide.save() await this.save(guide)
}
}
async save(guide: Guide) {
const storage = new Storage(path.dirname(guide.filepath))
const xmlFilepath = guide.filepath
const xmlFilename = path.basename(xmlFilepath)
this.logger.info(` saving to "${xmlFilepath}"...`)
const xmltv = guide.toString()
await storage.save(xmlFilename, xmltv)
if (guide.gzip) {
const zip = new Zip()
const compressed = zip.compress(xmltv)
const gzFilepath = `${guide.filepath}.gz`
const gzFilename = path.basename(gzFilepath)
this.logger.info(` saving to "${gzFilepath}"...`)
await storage.save(gzFilename, compressed)
} }
} }
} }
-3
View File
@@ -4,7 +4,6 @@ export * from './configLoader'
export * from './dataLoader' export * from './dataLoader'
export * from './dataProcessor' export * from './dataProcessor'
export * from './grabber' export * from './grabber'
export * from './guide'
export * from './guideManager' export * from './guideManager'
export * from './htmlTable' export * from './htmlTable'
export * from './issueLoader' export * from './issueLoader'
@@ -13,5 +12,3 @@ export * from './job'
export * from './proxyParser' export * from './proxyParser'
export * from './queue' export * from './queue'
export * from './queueCreator' export * from './queueCreator'
export * from './xml'
export * from './xmltv'
+1
View File
@@ -23,6 +23,7 @@ export class IssueLoader {
repo: REPO, repo: REPO,
per_page: 100, per_page: 100,
labels, labels,
state: 'open',
headers: { headers: {
'X-GitHub-Api-Version': '2022-11-28' 'X-GitHub-Api-Version': '2022-11-28'
} }
+12 -8
View File
@@ -2,9 +2,9 @@ import { URL } from 'node:url'
type ProxyParserResult = { type ProxyParserResult = {
protocol: string | null protocol: string | null
auth: { auth?: {
username: string | null username?: string
password: string | null password?: string
} }
host: string host: string
port: number | null port: number | null
@@ -14,14 +14,18 @@ export class ProxyParser {
parse(_url: string): ProxyParserResult { parse(_url: string): ProxyParserResult {
const parsed = new URL(_url) const parsed = new URL(_url)
return { const result: ProxyParserResult = {
protocol: parsed.protocol.replace(':', '') || null, protocol: parsed.protocol.replace(':', '') || null,
auth: {
username: parsed.username || null,
password: parsed.password || 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) {
result.auth = {}
if (parsed.username) result.auth.username = parsed.username
if (parsed.password) result.auth.password = parsed.password
}
return result
} }
} }
+10 -24
View File
@@ -1,15 +1,14 @@
import { Storage, Collection, DateTime, Logger } from '@freearhey/core' import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
import { ChannelsParser, ConfigLoader, Queue } from './'
import { SITES_DIR, DATA_DIR } from '../constants' import { SITES_DIR, DATA_DIR } from '../constants'
import { GrabOptions } from '../commands/epg/grab'
import { ConfigLoader, Queue } from './'
import { SiteConfig } from 'epg-grabber' import { SiteConfig } from 'epg-grabber'
import path from 'path' import path from 'path'
import { GrabOptions } from '../commands/epg/grab'
import { Channel } from '../models'
type QueueCreatorProps = { type QueueCreatorProps = {
logger: Logger logger: Logger
options: GrabOptions options: GrabOptions
parsedChannels: Collection channels: Collection
} }
export class QueueCreator { export class QueueCreator {
@@ -17,42 +16,29 @@ export class QueueCreator {
logger: Logger logger: Logger
sitesStorage: Storage sitesStorage: Storage
dataStorage: Storage dataStorage: Storage
parser: ChannelsParser channels: Collection
parsedChannels: Collection
options: GrabOptions options: GrabOptions
constructor({ parsedChannels, logger, options }: QueueCreatorProps) { constructor({ channels, logger, options }: QueueCreatorProps) {
this.parsedChannels = parsedChannels 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.parser = new ChannelsParser({ storage: new Storage() })
this.options = options this.options = options
this.configLoader = new ConfigLoader() this.configLoader = new ConfigLoader()
} }
async create(): Promise<Queue> { async create(): Promise<Queue> {
const channelsContent = await this.dataStorage.json('channels.json') let index = 0
const channels = new Collection(channelsContent).map(data => new Channel(data))
const queue = new Queue() const queue = new Queue()
for (const channel of this.parsedChannels.all()) { for (const channel of this.channels.all()) {
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) {
if (!channel.icon) {
const found: Channel = channels.first(
(_channel: Channel) => _channel.id === channel.xmltv_id
)
if (found) {
channel.icon = found.logo
}
}
} else {
channel.xmltv_id = channel.site_id channel.xmltv_id = channel.site_id
} }
-56
View File
@@ -1,56 +0,0 @@
import { Collection } from '@freearhey/core'
import { Channel } from 'epg-grabber'
export class XML {
items: Collection
constructor(items: Collection) {
this.items = items
}
toString() {
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.items.forEach((channel: Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id || ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
output += ` <channel site="${channel.site}" lang="${lang}" xmltv_id="${escapeString(
xmltv_id
)}" site_id="${site_id}"${logo}>${escapeString(channel.name)}</channel>\r\n`
})
output += '</channels>\r\n'
return output
}
}
function escapeString(value: string, defaultValue: string = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}
-28
View File
@@ -1,28 +0,0 @@
import { DateTime, Collection } from '@freearhey/core'
import { generateXMLTV } from 'epg-grabber'
type XMLTVProps = {
channels: Collection
programs: Collection
date: DateTime
}
export class XMLTV {
channels: Collection
programs: Collection
date: DateTime
constructor({ channels, programs, date }: XMLTVProps) {
this.channels = channels
this.programs = programs
this.date = date
}
toString() {
return generateXMLTV({
channels: this.channels.all(),
programs: this.programs.all(),
date: this.date.toJSON()
})
}
}
+68 -17
View File
@@ -1,26 +1,28 @@
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, Guide, Feed } 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 isNSFW: boolean = false
launched?: string launched?: string
closed?: string closed?: string
replacedBy?: string replacedBy?: string
website?: string website?: string
logo?: string
feeds?: Collection feeds?: Collection
logos: Collection = new Collection()
constructor(data?: ChannelData) {
if (!data) return
constructor(data: ChannelData) {
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)
@@ -35,11 +37,16 @@ export class Channel {
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
this.logo = data.logo
} }
withFeeds(feedsGroupedByChannelId: Dictionary): this { withFeeds(feedsGroupedByChannelId: Dictionary): this {
this.feeds = new Collection(feedsGroupedByChannelId.get(this.id)) if (this.id) this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
return this
}
withLogos(logosGroupedByChannelId: Dictionary): this {
if (this.id) this.logos = new Collection(logosGroupedByChannelId.get(this.id))
return this return this
} }
@@ -50,19 +57,19 @@ export class Channel {
return this.feeds return this.feeds
} }
getGuides(): Collection { getGuideChannels(): Collection {
let guides = new Collection() let channels = new Collection()
this.getFeeds().forEach((feed: Feed) => { this.getFeeds().forEach((feed: Feed) => {
guides = guides.concat(feed.getGuides()) channels = channels.concat(feed.getGuideChannels())
}) })
return guides return channels
} }
getGuideNames(): Collection { getGuideChannelNames(): Collection {
return this.getGuides() return this.getGuideChannels()
.map((guide: Guide) => guide.siteName) .map((channel: GuideChannel) => channel.siteName)
.uniq() .uniq()
} }
@@ -100,12 +107,56 @@ export class Channel {
return this.altNames || new Collection() return this.altNames || new Collection()
} }
getLogos(): Collection {
function feed(logo: Logo): number {
if (!logo.feed) return 1
if (logo.feed.isMain) return 1
return 0
}
function format(logo: Logo): number {
const levelByFormat: { [key: string]: number } = {
SVG: 0,
PNG: 3,
APNG: 1,
WebP: 1,
AVIF: 1,
JPEG: 2,
GIF: 1
}
return logo.format ? levelByFormat[logo.format] : 0
}
function size(logo: Logo): number {
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
}
return this.logos.orderBy([feed, format, size], ['desc', 'desc', 'asc'], false)
}
getLogo(): Logo | undefined {
return this.getLogos().first()
}
hasLogo(): boolean {
return this.getLogos().notEmpty()
}
getLogoUrl(): string {
const logo = this.getLogo()
if (!logo) return ''
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.getGuideNames().all(), guideNames: this.getGuideChannelNames().all(),
streamNames: this.getStreamNames().all(), streamNames: this.getStreamNames().all(),
feedFullNames: this.getFeedFullNames().all() feedFullNames: this.getFeedFullNames().all()
} }
+77
View File
@@ -0,0 +1,77 @@
import { Collection } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
export class ChannelList {
channels: Collection = new Collection()
constructor(data: { channels: epgGrabber.Channel[] }) {
this.channels = new Collection(data.channels)
}
add(channel: epgGrabber.Channel): this {
this.channels.add(channel)
return this
}
get(siteId: string): epgGrabber.Channel | undefined {
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
}
sort(): this {
this.channels = this.channels.orderBy([
(channel: epgGrabber.Channel) => channel.lang || '_',
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
(channel: epgGrabber.Channel) => channel.site_id
])
return this
}
toString() {
function escapeString(value: string, defaultValue: string = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.channels.forEach((channel: epgGrabber.Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
const site = channel.site || ''
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 += '</channels>\r\n'
return output
}
}
+60 -8
View File
@@ -1,6 +1,6 @@
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
import { FeedData } from '../types/feed' import { FeedData } from '../types/feed'
import { Channel } from './channel' import { Logo, Channel } from '.'
export class Feed { export class Feed {
channelId: string channelId: string
@@ -12,8 +12,9 @@ export class Feed {
languageCodes: Collection languageCodes: Collection
timezoneIds: Collection timezoneIds: Collection
videoFormat: string videoFormat: string
guides?: Collection guideChannels?: Collection
streams?: Collection streams?: Collection
logos: Collection = new Collection()
constructor(data: FeedData) { constructor(data: FeedData) {
this.channelId = data.channel this.channelId = data.channel
@@ -42,20 +43,30 @@ export class Feed {
return this return this
} }
withGuides(guidesGroupedByStreamId: Dictionary): this { withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`)) this.guideChannels = new Collection(
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
)
if (this.isMain) { if (this.isMain) {
this.guides = this.guides.concat(new Collection(guidesGroupedByStreamId.get(this.channelId))) this.guideChannels = this.guideChannels.concat(
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
)
} }
return this return this
} }
getGuides(): Collection { withLogos(logosGroupedByStreamId: Dictionary): this {
if (!this.guides) return new Collection() this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
return this.guides return this
}
getGuideChannels(): Collection {
if (!this.guideChannels) return new Collection()
return this.guideChannels
} }
getStreams(): Collection { getStreams(): Collection {
@@ -69,4 +80,45 @@ export class Feed {
return `${this.channel.name} ${this.name}` return `${this.channel.name} ${this.name}`
} }
getStreamId(): string {
return `${this.channelId}@${this.id}`
}
getLogos(): Collection {
function format(logo: Logo): number {
const levelByFormat: { [key: string]: number } = {
SVG: 0,
PNG: 3,
APNG: 1,
WebP: 1,
AVIF: 1,
JPEG: 2,
GIF: 1
}
return logo.format ? levelByFormat[logo.format] : 0
}
function size(logo: Logo): number {
return Math.abs(512 - logo.width) + Math.abs(512 - logo.height)
}
return this.logos.orderBy([format, size], ['desc', 'asc'], false)
}
getLogo(): Logo | undefined {
return this.getLogos().first()
}
hasLogo(): boolean {
return this.getLogos().notEmpty()
}
getLogoUrl(): string {
const logo = this.getLogo()
if (!logo) return ''
return logo.url || ''
}
} }
+27 -27
View File
@@ -1,35 +1,35 @@
import type { GuideData } from '../types/guide' import { Collection, DateTime } from '@freearhey/core'
import { uniqueId } from 'lodash' import { generateXMLTV } from 'epg-grabber'
type GuideData = {
channels: Collection
programs: Collection
filepath: string
gzip: boolean
}
export class Guide { export class Guide {
channelId?: string channels: Collection
feedId?: string programs: Collection
siteDomain?: string filepath: string
siteId?: string gzip: boolean
siteName?: string
languageCode?: string
constructor(data?: GuideData) { constructor({ channels, programs, filepath, gzip }: GuideData) {
if (!data) return this.channels = channels
this.programs = programs
this.channelId = data.channel this.filepath = filepath
this.feedId = data.feed this.gzip = gzip || false
this.siteDomain = data.site
this.siteId = data.site_id
this.siteName = data.site_name
this.languageCode = data.lang
} }
getUUID(): string { toString() {
if (!this.getStreamId() || !this.siteId) return uniqueId() const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
timezone: 'UTC'
})
return this.getStreamId() + this.siteId return generateXMLTV({
} channels: this.channels.all(),
programs: this.programs.all(),
getStreamId(): string | undefined { date: currDate.toJSON()
if (!this.channelId) return undefined })
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
} }
} }
+59
View File
@@ -0,0 +1,59 @@
import { Dictionary } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
import { Feed, Channel } from '.'
export class GuideChannel {
channelId?: string
channel?: Channel
feedId?: string
feed?: Feed
xmltvId?: string
languageCode?: string
siteId?: string
logoUrl?: string
siteDomain?: string
siteName?: string
constructor(data: epgGrabber.Channel) {
const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined]
this.channelId = channelId
this.feedId = feedId
this.xmltvId = data.xmltv_id
this.languageCode = data.lang
this.siteId = data.site_id
this.logoUrl = data.logo
this.siteDomain = data.site
this.siteName = data.name
}
withChannel(channelsKeyById: Dictionary): this {
if (this.channelId) this.channel = channelsKeyById.get(this.channelId)
return this
}
withFeed(feedsKeyByStreamId: Dictionary): this {
if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId())
return this
}
getStreamId(): string {
if (!this.channelId) return ''
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
toJSON() {
return {
channel: this.channelId || null,
feed: this.feedId || null,
site: this.siteDomain || '',
site_id: this.siteId || '',
site_name: this.siteName || '',
lang: this.languageCode || ''
}
}
}
+6 -3
View File
@@ -1,6 +1,9 @@
export * from './issue'
export * from './site'
export * from './channel' export * from './channel'
export * from './feed' export * from './feed'
export * from './stream'
export * from './guide' export * from './guide'
export * from './guideChannel'
export * from './issue'
export * from './logo'
export * from './site'
export * from './stream'
export * from './channelList'
+41
View File
@@ -0,0 +1,41 @@
import { Collection, type Dictionary } from '@freearhey/core'
import type { LogoData } from '../types/logo'
import { type Feed } from './feed'
export class Logo {
channelId?: string
feedId?: string
feed?: Feed
tags: Collection = new Collection()
width: number = 0
height: number = 0
format?: string
url?: string
constructor(data?: LogoData) {
if (!data) return
this.channelId = data.channel
this.feedId = data.feed || undefined
this.tags = new Collection(data.tags)
this.width = data.width
this.height = data.height
this.format = data.format || undefined
this.url = data.url
}
withFeed(feedsKeyByStreamId: Dictionary): this {
if (!this.feedId) return this
this.feed = feedsKeyByStreamId.get(this.getStreamId())
return this
}
getStreamId(): string {
if (!this.channelId) return ''
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
}
-1
View File
@@ -15,7 +15,6 @@ export type ChannelData = {
closed: string closed: string
replaced_by: string replaced_by: string
website: string website: string
logo: string
} }
export type ChannelSearchableData = { export type ChannelSearchableData = {
+1
View File
@@ -16,4 +16,5 @@ export type DataLoaderData = {
timezones: object | object[] timezones: object | object[]
guides: object | object[] guides: object | object[]
streams: object | object[] streams: object | object[]
logos: object | object[]
} }
+6 -2
View File
@@ -1,12 +1,16 @@
import { Collection, Dictionary } from '@freearhey/core' import { Collection, Dictionary } from '@freearhey/core'
export type DataProcessorData = { export type DataProcessorData = {
guideChannelsGroupedByStreamId: Dictionary
feedsGroupedByChannelId: Dictionary feedsGroupedByChannelId: Dictionary
guidesGroupedByStreamId: Dictionary logosGroupedByChannelId: Dictionary
logosGroupedByStreamId: Dictionary
feedsKeyByStreamId: Dictionary
streamsGroupedById: Dictionary streamsGroupedById: Dictionary
channelsKeyById: Dictionary channelsKeyById: Dictionary
guideChannels: Collection
channels: Collection channels: Collection
streams: Collection streams: Collection
guides: Collection
feeds: Collection feeds: Collection
logos: Collection
} }
+9
View File
@@ -0,0 +1,9 @@
export type LogoData = {
channel: string
feed: string | null
tags: string[]
width: number
height: number
format: string | null
url: string
}
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?><tv date="20221020"> <?xml version="1.0" encoding="UTF-8" ?><tv date="20221020">
<channel id="Channel2.us"><display-name>Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url><lcn>36</lcn></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel> <channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel2.us"><display-name>Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url></channel>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme> <programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221020043100 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
</tv> </tv>
@@ -1,14 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?><tv date="20221020"> <?xml version="1.0" encoding="UTF-8" ?><tv date="20221020">
<channel id="Channel1.us"><display-name>Custom Channel 1</display-name><url>https://example.com</url></channel> <channel id="Channel1.us"><display-name>Custom Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example2.com</url></channel>
<channel id="Channel2.us"><display-name>Custom Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url></channel> <channel id="Channel2.us"><display-name>Custom Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url></channel>
<channel id="Channel3.us"><display-name>Channel 3</display-name><icon src="https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"/><url>https://example2.com</url></channel> <channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel4.us"><display-name>Channel 4</display-name><url>https://example2.com</url></channel> <channel id="Channel3.us@Wrong"><display-name>Channel 3</display-name><icon src="https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"/><url>https://example2.com</url></channel>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme> <channel id="Channel4.us@HD"><display-name>Channel 4</display-name><icon src="https://i.imgur.com/BPzH88J.png"/><url>https://example2.com</url></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example2.com</url></channel>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221019044000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme> <programme start="20221019044000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel3.us"><title lang="en">Program1 (example2.com)</title></programme> <programme start="20221020043100 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel4.us"><title lang="en">Program1 (example2.com)</title></programme> <programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel3.us@Wrong"><title lang="en">Program1 (example2.com)</title></programme>
<programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel4.us@HD"><title lang="en">Program1 (example2.com)</title></programme>
</tv> </tv>
Binary file not shown.
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?><tv date="20221020"> <?xml version="1.0" encoding="UTF-8" ?><tv date="20221020">
<channel id="Channel2.us"><display-name>Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url><lcn>36</lcn></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel> <channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel2.us"><display-name>Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url></channel> <programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme> <programme start="20221020043100 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
</tv> </tv>
@@ -3,6 +3,6 @@
<channel id="Channel3.us"><display-name>Channel 3</display-name><icon src="https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"/><url>https://example.com</url></channel> <channel id="Channel3.us"><display-name>Channel 3</display-name><icon src="https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"/><url>https://example.com</url></channel>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel3.us"><title lang="it">Program1 (example.com)</title></programme> <programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel3.us"><title lang="it">Program1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel3.us"><title lang="it">Program1 (example.com)</title></programme> <programme start="20221020043100 +0000" stop="20221020071000 +0000" channel="Channel3.us"><title lang="it">Program1 (example.com)</title></programme>
</tv> </tv>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?><tv date="20221020">
<channel id="Channel2.us"><display-name>Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url><lcn>36</lcn></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221020043100 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
</tv>
@@ -1,14 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?><tv date="20221020"> <?xml version="1.0" encoding="UTF-8" ?><tv date="20221020">
<channel id="Channel2.us"><display-name>Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url><lcn>36</lcn></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel> <channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example.com</url></channel>
<channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example2.com</url></channel>
<channel id="Channel2.us"><display-name>Channel 2</display-name><icon src="https://i.imgur.com/qmRnD0M.png"/><url>https://example.com</url></channel>
<channel id="Channel3.us"><display-name>Channel 3</display-name><icon src="https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"/><url>https://example2.com</url></channel> <channel id="Channel3.us"><display-name>Channel 3</display-name><icon src="https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"/><url>https://example2.com</url></channel>
<channel id="Channel4.us"><display-name>Channel 4</display-name><url>https://example2.com</url></channel> <channel id="Channel4.us@HD"><display-name>Channel 4</display-name><icon src="https://i.imgur.com/BPzH88J.png"/><url>https://example2.com</url></channel>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme> <channel id="Channel1.us"><display-name>Channel 1</display-name><url>https://example2.com</url></channel>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221019044000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme> <programme start="20221019044000 +0000" stop="20221019071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example2.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme> <programme start="20221020043000 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="fr">Programme1 (example.com)</title></programme>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel3.us"><title lang="en">Program1 (example2.com)</title></programme> <programme start="20221020043100 +0000" stop="20221020071000 +0000" channel="Channel1.us"><title lang="en">Program1 (example.com)</title></programme>
<programme start="20221019043000 +0000" stop="20221019071000 +0000" channel="Channel4.us"><title lang="en">Program1 (example2.com)</title></programme> <programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel3.us"><title lang="en">Program1 (example2.com)</title></programme>
<programme start="20221019043100 +0000" stop="20221019071000 +0000" channel="Channel4.us@HD"><title lang="en">Program1 (example2.com)</title></programme>
</tv> </tv>
+9 -14
View File
@@ -9,8 +9,7 @@
"categories": [], "categories": [],
"is_nsfw": false, "is_nsfw": false,
"closed": "2020-01-01", "closed": "2020-01-01",
"replaced_by": "R6.co", "replaced_by": "R6.co"
"logo": "https://www.directv.com/images/logos/channels/dark/large/579.png"
}, },
{ {
"id": "Bravos.us", "id": "Bravos.us",
@@ -20,8 +19,7 @@
"subdivision": null, "subdivision": null,
"city": null, "city": null,
"categories": [], "categories": [],
"is_nsfw": false, "is_nsfw": false
"logo": "https://www.directv.com/images/logos/channels/dark/large/579.png"
}, },
{ {
"id": "CNNInternational.us", "id": "CNNInternational.us",
@@ -34,8 +32,7 @@
"categories": [ "categories": [
"news" "news"
], ],
"is_nsfw": false, "is_nsfw": false
"logo": "https://i.imgur.com/2BXCg0x.jpg"
}, },
{ {
"id": "MNetMovies2.za", "id": "MNetMovies2.za",
@@ -45,11 +42,10 @@
"subdivision": null, "subdivision": null,
"city": null, "city": null,
"categories": [], "categories": [],
"is_nsfw": false, "is_nsfw": false
"logo": "https://rndcdn.dstv.com/dstvcms/2020/08/31/M-Net_Movies_2_Logo_4-3_lightbackground_xlrg.png"
}, },
{"id":"6eren.dk","name":"6'eren","alt_names":[],"network":null,"owners":["Warner Bros. Discovery EMEA"],"country":"DK","subdivision":null,"city":null,"broadcast_area":["c/DK"],"languages":["dan"],"categories":[],"is_nsfw":false,"launched":"2009-01-01","closed":null,"replaced_by":null,"website":"http://www.6-eren.dk/","logo":"https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"}, {"id":"6eren.dk","name":"6'eren","alt_names":[],"network":null,"owners":["Warner Bros. Discovery EMEA"],"country":"DK","subdivision":null,"city":null,"broadcast_area":["c/DK"],"languages":["dan"],"categories":[],"is_nsfw":false,"launched":"2009-01-01","closed":null,"replaced_by":null,"website":"http://www.6-eren.dk/"},
{"id":"BBCNews.uk","name":"BBC News","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/UK"],"languages":["eng"],"categories":["news"],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"http://news.bbc.co.uk/","logo":"https://i.imgur.com/rPzH88J.png"}, {"id":"BBCNews.uk","name":"BBC News","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/UK"],"languages":["eng"],"categories":["news"],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"http://news.bbc.co.uk/"},
{ {
"id": "CNN.us", "id": "CNN.us",
"name": "CNN", "name": "CNN",
@@ -58,9 +54,8 @@
"subdivision": null, "subdivision": null,
"city": null, "city": null,
"categories": [], "categories": [],
"is_nsfw": false, "is_nsfw": false
"logo": "https://www.directv.com/images/logos/channels/dark/large/579.png"
}, },
{"id":"Channel2.us","name":"Channel 2 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"","logo":"https://i.imgur.com/rPzH88J.png"}, {"id":"Channel2.us","name":"Channel 2 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""},
{"id":"Channel3.us","name":"Channel 3 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":"","logo":"https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"} {"id":"Channel3.us","name":"Channel 3 [API]","alt_names":[],"network":null,"owners":[],"country":"UK","subdivision":null,"city":null,"broadcast_area":["c/US"],"languages":["eng"],"categories":[],"is_nsfw":false,"launched":null,"closed":null,"replaced_by":null,"website":""}
] ]
+28
View File
@@ -26,5 +26,33 @@
], ],
"languages": [], "languages": [],
"video_format": "480i" "video_format": "480i"
},
{
"channel": "Bravo.us",
"id": "East",
"name": "East",
"is_main": true,
"broadcast_area": [
"r/EUR"
],
"timezones": [
"America/New_York"
],
"languages": [],
"video_format": "480i"
},
{
"channel": "Channel4.us",
"id": "HD",
"name": "HD",
"is_main": true,
"broadcast_area": [
"r/EUR"
],
"timezones": [
"America/New_York"
],
"languages": [],
"video_format": "480i"
} }
] ]
+4
View File
@@ -0,0 +1,4 @@
[
{"channel":"Channel3.us","feed":null,"tags":[],"width":334,"height":210,"format":"PNG","url":"https://upload.wikimedia.org/wikipedia/commons/6/64/6%27eren_2015.png"},
{"channel":"Channel4.us","feed":"HD","tags":[],"width":334,"height":210,"format":"PNG","url":"https://i.imgur.com/BPzH88J.png"}
]
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels> <channels>
<channel site="example.com" lang="en" xmltv_id="Bravo.us@East" site_id="140#Tes't">Bravo's</channel> <channel site="example.com" lang="en" xmltv_id="Bravo.us@East" site_id="140#Tes't" lcn="36">Bravo's</channel>
</channels> </channels>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels site="example.com"> <channels site="example.com">
<channel lang="en" xmltv_id="CNNInternational" site_id="140">CNN International</channel> <channel lang="en" xmltv_id="CNNInternational" site_id="140">CNN International</channel>
<channel lang="en" xmltv_id="Bravo.us@East" site_id="150">Bravo</channel> <channel lang="en" xmltv_id="Bravo.us" site_id="150">Bravo</channel>
<channel lang="en" xmltv_id="Bravo.us@West" site_id="151">Bravo</channel> <channel lang="en" xmltv_id="Bravo.us" site_id="151">Bravo</channel>
</channels> </channels>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels site="example.com">
<channel lang="en" xmltv_id="Bravo.us@West" site_id="150">Bravo</channel>
</channels>
@@ -3,7 +3,7 @@
<channel site="example.com" lang="en" xmltv_id="Channel1.us" site_id="140">Custom Channel 1</channel> <channel site="example.com" lang="en" xmltv_id="Channel1.us" site_id="140">Custom Channel 1</channel>
<channel site="example.com" lang="en" xmltv_id="Channel2.us" site_id="142" logo="https://i.imgur.com/qmRnD0M.png">Custom Channel 2</channel> <channel site="example.com" lang="en" xmltv_id="Channel2.us" site_id="142" logo="https://i.imgur.com/qmRnD0M.png">Custom Channel 2</channel>
<channel site="example.com" lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel> <channel site="example.com" lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel>
<channel site="example2.com" lang="en" xmltv_id="Channel3.us" site_id="150">Channel 3</channel> <channel site="example2.com" lang="en" xmltv_id="Channel3.us@Wrong" site_id="150">Channel 3</channel>
<channel site="example2.com" lang="en" xmltv_id="Channel4.us" site_id="152">Channel 4</channel> <channel site="example2.com" lang="en" xmltv_id="Channel4.us@HD" site_id="152">Channel 4</channel>
<channel site="example2.com" lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel> <channel site="example2.com" lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel>
</channels> </channels>
@@ -20,7 +20,7 @@ module.exports = {
return [ return [
{ {
title: 'Program1 (example.com)', title: 'Program1 (example.com)',
start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`,
stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z`
} }
] ]
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels site="example.com"> <channels site="example.com">
<channel lang="en" xmltv_id="Channel2.us" site_id="142" logo="https://i.imgur.com/qmRnD0M.png" lcn="36">Channel 2</channel>
<channel lang="en" xmltv_id="Channel1.us" site_id="140">Channel 1</channel> <channel lang="en" xmltv_id="Channel1.us" site_id="140">Channel 1</channel>
<channel lang="en" xmltv_id="Channel2.us" site_id="142" logo="https://i.imgur.com/qmRnD0M.png">Channel 2</channel>
<channel lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel> <channel lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel>
</channels> </channels>
@@ -20,7 +20,7 @@ module.exports = {
return [ return [
{ {
title: 'Program1 (example.com)', title: 'Program1 (example.com)',
start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`,
stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z`
} }
] ]
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<channels site="example2.com"> <channels site="example2.com">
<channel lang="en" xmltv_id="Channel3.us" site_id="150">Channel 3</channel> <channel lang="en" xmltv_id="Channel3.us" site_id="150">Channel 3</channel>
<channel lang="en" xmltv_id="Channel4.us" site_id="152">Channel 4</channel> <channel lang="en" xmltv_id="Channel4.us@HD" site_id="152">Channel 4</channel>
<channel lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel> <channel lang="fr" xmltv_id="Channel1.us" site_id="140">Channel 1</channel>
</channels> </channels>
@@ -15,7 +15,7 @@ module.exports = {
return [ return [
{ {
title: 'Program1 (example2.com)', title: 'Program1 (example2.com)',
start: `${date.format('YYYY-MM-DD')}T04:30:00.000Z`, start: `${date.format('YYYY-MM-DD')}T04:31:00.000Z`,
stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z` stop: `${date.format('YYYY-MM-DD')}T07:10:00.000Z`
} }
] ]
+27 -7
View File
@@ -28,20 +28,40 @@ describe('channels:validate', () => {
} }
}) })
it('will show a message if the file contains a channel with wrong xmltv_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_xmltv_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_xmltv_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │ │ 0 │ 'wrong_channel_id' │ 'en' │ 'CNNInternational' │ '140' │ 'CNN International' │
└─────────┴──────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘ └─────────┴────────────────────┴──────┴────────────────────┴─────────┴─────────────────────┘
1 error(s) in 1 file(s)
`)
}
})
it('will show a message if the file contains a channel with wrong feed id', () => {
try {
const cmd = `${ENV_VAR} npm run channels:validate --- tests/__data__/input/channels_validate/wrong_feed_id.channels.xml`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
process.exit(1)
} catch (error) {
expect((error as ExecError).status).toBe(1)
expect((error as ExecError).stdout).toContain(`
┌─────────┬─────────────────┬──────┬─────────────────┬─────────┬─────────┐
│ (index) │ type │ lang │ xmltv_id │ site_id │ name │
├─────────┼─────────────────┼──────┼─────────────────┼─────────┼─────────┤
│ 0 │ 'wrong_feed_id' │ 'en' │ 'Bravo.us@West' │ '150' │ 'Bravo' │
└─────────┴─────────────────┴──────┴─────────────────┴─────────┴─────────┘
1 error(s) in 1 file(s) 1 error(s) in 1 file(s)
`) `)
+91 -80
View File
@@ -4,7 +4,8 @@ import { Zip } from '@freearhey/core'
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
const ENV_VAR = 'cross-env SITES_DIR=tests/__data__/input/epg_grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/__data__' const ENV_VAR =
'cross-env SITES_DIR=tests/__data__/input/epg_grab/sites CURR_DATE=2022-10-20 DATA_DIR=tests/__data__/input/__data__'
beforeEach(() => { beforeEach(() => {
fs.emptyDirSync('tests/__data__/output') fs.emptyDirSync('tests/__data__/output')
@@ -14,85 +15,12 @@ describe('epg:grab', () => {
it('can grab epg by site name', () => { it('can grab epg by site name', () => {
const cmd = `${ENV_VAR} npm run grab --- --site=example.com --output="${path.resolve( const cmd = `${ENV_VAR} npm run grab --- --site=example.com --output="${path.resolve(
'tests/__data__/output/guide.xml' 'tests/__data__/output/guide.xml'
)}"` )}" --timeout=100`
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)
expect(content('tests/__data__/output/guide.xml')).toEqual( expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guide_2.xml') content('tests/__data__/expected/epg_grab/base.guide.xml')
)
})
it('can grab epg with multiple channels.xml files', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guide.xml')
)
})
it('can grab epg with gzip option enabled', async () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output="${path.resolve(
'tests/__data__/output/guide.xml'
)}" --gzip`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guide.xml')
)
const zip = new Zip()
const expected = zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz'))
const result = zip.decompress(
fs.readFileSync('tests/__data__/expected/epg_grab/guide.xml.gz')
)
expect(expected).toEqual(result)
}, 30000)
it('can grab epg with wildcard as output', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels="tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml" --output="tests/__data__/output/guides/{lang}/{site}.xml"`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guides/en/example.com.xml')
)
expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml')
)
})
it('can grab epg then language filter enabled', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml')
)
})
it('can grab epg then using a multi-language filter', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{site}.xml --lang=fr,it`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guides/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guide_4.xml')
)
})
it('can grab epg using custom channels list', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guide_3.xml')
) )
}) })
@@ -119,29 +47,112 @@ describe('epg:grab', () => {
expect(errorThrown).toBe(true) expect(errorThrown).toBe(true)
}) })
it('can grab epg with wildcard as output', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels="tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml" --output="tests/__data__/output/guides/{lang}/{site}.xml" --timeout=100`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guides/en/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guides/en/example.com.xml')
)
expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml')
)
})
it('can grab epg then language filter enabled', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{lang}/{site}.xml --lang=fr --timeout=100`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guides/fr/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guides/fr/example.com.xml')
)
})
it('can grab epg then using a multi-language filter', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/example.com/example.com.channels.xml --output=tests/__data__/output/guides/{site}.xml --lang=fr,it --timeout=100`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guides/example.com.xml')).toEqual(
content('tests/__data__/expected/epg_grab/lang.guide.xml')
)
})
it('can grab epg via https proxy', () => { it('can grab epg via https proxy', () => {
const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=https://bob:123456@proxy.com:1234 --output="${path.resolve( const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=https://bob:123456@proxy.com:1234 --output="${path.resolve(
'tests/__data__/output/guide.xml' 'tests/__data__/output/guide.xml'
)}"` )}" --timeout=100`
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)
expect(content('tests/__data__/output/guide.xml')).toEqual( expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guide_2.xml') content('tests/__data__/expected/epg_grab/proxy.guide.xml')
) )
}) })
it('can grab epg via socks5 proxy', () => { it('can grab epg via socks5 proxy', () => {
const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=socks5://bob:123456@proxy.com:1234 --output="${path.resolve( const cmd = `${ENV_VAR} npm run grab --- --site=example.com --proxy=socks5://bob:123456@proxy.com:1234 --output="${path.resolve(
'tests/__data__/output/guide.xml' 'tests/__data__/output/guide.xml'
)}"` )}" --timeout=100`
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)
expect(content('tests/__data__/output/guide.xml')).toEqual( expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/guide_2.xml') content('tests/__data__/expected/epg_grab/proxy.guide.xml')
) )
}) })
it('can grab epg with curl option', () => {
const cmd = `${ENV_VAR} npm run grab --- --site=example.com --curl --output="${path.resolve(
'tests/__data__/output/guide.xml'
)}" --timeout=100`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(stdout).toContain('curl https://example.com')
})
it('can grab epg with multiple channels.xml files', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output=tests/__data__/output/guide.xml --timeout=100`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/template.guide.xml')
)
})
it('can grab epg using custom channels list', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/custom.channels.xml --output=tests/__data__/output/guide.xml --timeout=100`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/custom_channels.guide.xml')
)
})
it('can grab epg with gzip option enabled', () => {
const cmd = `${ENV_VAR} npm run grab --- --channels=tests/__data__/input/epg_grab/sites/**/*.channels.xml --output="${path.resolve(
'tests/__data__/output/guide.xml'
)}" --gzip --timeout=100`
const stdout = execSync(cmd, { encoding: 'utf8' })
if (process.env.DEBUG === 'true') console.log(cmd, stdout)
expect(content('tests/__data__/output/guide.xml')).toEqual(
content('tests/__data__/expected/epg_grab/template.guide.xml')
)
const zip = new Zip()
const expected = zip.decompress(fs.readFileSync('tests/__data__/output/guide.xml.gz'))
const result = zip.decompress(
fs.readFileSync('tests/__data__/expected/epg_grab/template.guide.xml.gz')
)
expect(expected).toEqual(result)
})
}) })
function content(filepath: string) { function content(filepath: string) {
-8440
View File
File diff suppressed because it is too large Load Diff