mirror of
https://github.com/iptv-org/epg
synced 2026-09-15 05:26:25 -04:00
Merge branch 'master' into zap2it.com
This commit is contained in:
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"es2021": true,
|
|
||||||
"jest": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"no-case-declarations": "off",
|
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"windows"
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"single",
|
|
||||||
{
|
|
||||||
"avoidEscape": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"never"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Enforce the usage of CRLF in GitHub Actions per ESLint configuration.
|
||||||
|
* text eol=crlf
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: ✍️ Channel Request
|
||||||
|
description: Request to add a channel to the guide
|
||||||
|
labels: ['channel request']
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please fill out the request as much as possible so we can efficiently process your request.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Site
|
||||||
|
description: The name of the site
|
||||||
|
placeholder: 'guidatv.sky.it'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Channels
|
||||||
|
description: List of channels to be added
|
||||||
|
placeholder: 'BBC One'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Notes
|
||||||
|
description: Anything else we should know?
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: ✨ Feature request
|
||||||
|
description: Ask for a feature to be added to the guide
|
||||||
|
labels: ['feature request']
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please fill out the issue template as much as you can so we could efficiently process your request.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Site
|
||||||
|
description: The name of the site
|
||||||
|
placeholder: 'guidatv.sky.it'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Detailed description of the feature.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 💡 Feature request
|
|
||||||
url: https://github.com/orgs/iptv-org/discussions/categories/ideas
|
|
||||||
about: For any ideas or feature requests
|
|
||||||
- name: ❓ Ask a question
|
- name: ❓ Ask a question
|
||||||
url: https://github.com/orgs/iptv-org/discussions/categories/q-a
|
url: https://github.com/orgs/iptv-org/discussions/categories/q-a
|
||||||
about: Ask questions about this project
|
about: Ask questions about this project
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: check
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: changed files
|
||||||
|
id: files
|
||||||
|
run: |
|
||||||
|
git fetch origin master:master
|
||||||
|
JS_ANY_CHANGED=false
|
||||||
|
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
|
||||||
|
JS_ANY_CHANGED=true
|
||||||
|
fi
|
||||||
|
echo "js_all_changed_files=$JS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "js_any_changed=$JS_ANY_CHANGED" >> "$GITHUB_OUTPUT"
|
||||||
|
CHANNELS_ANY_CHANGED=false
|
||||||
|
CHANNELS_ALL_CHANGED_FILES=$(git diff --name-only master -- sites/**/*.channels.xml | tr '\n' ' ')
|
||||||
|
if [ -n "${CHANNELS_ALL_CHANGED_FILES}" ]; then
|
||||||
|
CHANNELS_ANY_CHANGED=true
|
||||||
|
fi
|
||||||
|
echo "channels_all_changed_files=$CHANNELS_ALL_CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "channels_any_changed=$CHANNELS_ANY_CHANGED" >> "$GITHUB_OUTPUT"
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
if: ${{ steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true' }}
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'npm'
|
||||||
|
- name: install dependencies
|
||||||
|
if: steps.files.outputs.js_any_changed == 'true' || steps.files.outputs.channels_any_changed == 'true'
|
||||||
|
run: SKIP_POSTINSTALL=1 npm install
|
||||||
|
- name: check changed js-files
|
||||||
|
if: steps.files.outputs.js_any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
npx eslint ${{ steps.files.outputs.js_all_changed_files }}
|
||||||
|
- name: check changed *.channels.xml
|
||||||
|
if: steps.files.outputs.channels_any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
npm run channels:lint -- ${{ steps.files.outputs.channels_all_changed_files }}
|
||||||
|
npm run postinstall
|
||||||
|
npm run channels:validate -- ${{ steps.files.outputs.channels_all_changed_files }}
|
||||||
@@ -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,16 +22,28 @@ 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: 18
|
node-version: 22
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: check *.channels.xml
|
- name: check *.channels.xml
|
||||||
run: |
|
run: |
|
||||||
npm run channels:lint
|
npm run channels:lint
|
||||||
|
- name: update sites.md
|
||||||
|
run: npm run sites:update
|
||||||
|
- run: git status
|
||||||
|
- name: commit changes to sites.md
|
||||||
|
if: ${{ !env.ACT && github.ref == 'refs/heads/master' }}
|
||||||
|
run: |
|
||||||
|
SITE=SITES.md
|
||||||
|
CHANGED=$(git diff ${SITE})
|
||||||
|
if [ -n "${CHANGED}" ]; then
|
||||||
|
git add ${SITE}
|
||||||
|
git commit -m "[Bot] Update ${SITE}" -m "Committed by [iptv-bot](https://github.com/apps/iptv-bot) via [update](https://github.com/iptv-org/epg/actions/runs/${{ github.run_id }}) workflow." --no-verify
|
||||||
|
git push
|
||||||
|
fi
|
||||||
- name: generate .api/guides.json
|
- name: generate .api/guides.json
|
||||||
run: npm run api:generate
|
run: npm run api:generate
|
||||||
- run: git status
|
- run: git status
|
||||||
|
|||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
channels_changed="$(git diff --staged --name-only --diff-filter=ACMR -- 'sites/**/*.channels.xml' | sed 's| |\\ |g')"
|
||||||
|
|
||||||
|
if [ ! -z "$channels_changed" ]; then
|
||||||
|
npm run channels:lint -- $channels_changed
|
||||||
|
fi
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
channels_changed="$(git diff --staged --name-only --diff-filter=ACMR -- 'sites/**/*.channels.xml' | sed 's| |\\ |g')"
|
||||||
|
|
||||||
|
if [ ! -z "$channels_changed" ]; then
|
||||||
|
npm run channels:validate -- $channels_changed
|
||||||
|
fi
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.husky/scripts_lint.sh
|
||||||
|
.husky/channels_lint.sh
|
||||||
|
.husky/channels_validate.sh
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
scripts_changed="$(git diff --staged --name-only --diff-filter=ACMR -- 'tests/**/*.ts' 'tests/**/*.js' 'scripts/**/*.ts' 'scripts/**/*.mts' 'scripts/**/*.js' 'sites/**/*.js' 'sites/**/*.ts' | sed 's| |\\ |g')"
|
||||||
|
|
||||||
|
if [ ! -z "$scripts_changed" ]; then
|
||||||
|
npx eslint $scripts_changed
|
||||||
|
fi
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
endOfLine: 'lf',
|
endOfLine: 'crlf',
|
||||||
semi: false,
|
semi: false,
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
printWidth: 100,
|
printWidth: 100,
|
||||||
|
|||||||
+319
-42
@@ -8,47 +8,247 @@
|
|||||||
|
|
||||||
### How to add a channel to the guide?
|
### How to add a channel to the guide?
|
||||||
|
|
||||||
Open the [/sites](/sites) folder and select the source that you know has the guide for the channel you want.
|
To ask for help with adding a channel simply fill out this [form](https://github.com/iptv-org/epg/issues/new?assignees=&labels=channel+request&projects=&template=2_channel-request.yml).
|
||||||
|
|
||||||
Then in the selected folder open the file `*.channels.xml` and add to it:
|
If you want to add a channel to the list yourself, here are the instructions on how to do it.
|
||||||
|
|
||||||
|
First select the site from [SITES.md](SITES.md) that you know has a guide for the channel you need. Then go to the folder with its config and open the file `*.channels.xml`.
|
||||||
|
|
||||||
|
Make sure that the desired channel is not already in the list. If it is not, simply add its description to the end of the list as shown here:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<channel site="SITE" lang="LANGUAGE_CODE" xmltv_id="CHANNEL_ID" site_id="SITE_ID">CHANNEL_NAME</channel>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
...
|
||||||
|
<channel site="SITE" lang="LANGUAGE_CODE" xmltv_id="CHANNEL_ID" site_id="SITE_ID">CHANNEL_NAME</channel>
|
||||||
|
</channels>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Attribute | Description | Example |
|
| Attribute | Description | Example |
|
||||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
|
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||||
| SITE | Site domain name. | `example.com` |
|
| SITE | Site domain name. | `example.com` |
|
||||||
| LANGUAGE_CODE | Language of the guide ([ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code). | `en` |
|
| LANGUAGE_CODE | Language of the guide ([ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code). | `en` |
|
||||||
| CHANNEL_ID | Channel ID from [iptv-org/database](https://github.com/iptv-org/database). A complete list of supported channels can also be found at https://iptv-org.github.io/. | `BBCOne.uk` |
|
| CHANNEL_ID | ID of the channel. Full list of supported channels with corresponding ID could be found on [iptv-org.github.io](https://iptv-org.github.io/). | `HBO.us@East` |
|
||||||
| SITE_ID | Unique ID of the channel used in the source. | `bbc1` |
|
| SITE_ID | Unique ID of the channel used in the source. | `hbo` |
|
||||||
| CHANNEL_NAME | Name of the channel used in the source. | `BBC 1` |
|
| CHANNEL_NAME | Name of the channel used in the source. | `HBO East` |
|
||||||
|
|
||||||
After that just commit all changes and send a pull request.
|
After that just [commit](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/about-commits) all changes and send a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests).
|
||||||
|
|
||||||
|
### How to report broken guide?
|
||||||
|
|
||||||
|
If you start to get errors when downloading the guide or if nothing loads at all, please let us know via this [form](https://github.com/iptv-org/epg/issues/new?assignees=&labels=broken+guide&projects=&template=3_broken-guide.yml).
|
||||||
|
|
||||||
### How to add a new source to the repository?
|
### How to add a new source to the repository?
|
||||||
|
|
||||||
To do this, you must create a new folder in the [/sites](/sites) with at least 3 files:
|
If you are not familiar with javascript programming, you can ask for help from other community members through this [form](https://github.com/iptv-org/epg/issues/new?assignees=&labels=source+request&projects=&template=1_source-request.yml). Otherwise, below are the instructions for you.
|
||||||
|
|
||||||
|
To start with, you need to create a new folder in the [/sites](/sites) folder and put at least 4 files in it:
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>example.com.config.js</summary>
|
<summary>example.com.config.js</summary>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
This file describes what kind of request we need to send to get the guide for a particular channel on a certain date. It also describes how to parse the response.
|
This file describes what kind of request we need to send to get the guide for a particular channel on a certain date and how to parse the response.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'example.com',
|
site: 'example.com',
|
||||||
url: function ({ channel, date }) {
|
url({ channel, date }) {
|
||||||
return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}`
|
return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}`
|
||||||
},
|
},
|
||||||
parser: function ({ content }) {
|
parser(context) {
|
||||||
return JSON.parse(content)
|
try {
|
||||||
|
return JSON.parse(context.content)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
More detailed instructions for this file can be found here: https://github.com/freearhey/epg-grabber#site-config
|
### Context Object
|
||||||
|
|
||||||
|
From each function in `config.js` you can access a `context` object containing the following data:
|
||||||
|
|
||||||
|
- `channel`: The object describing the current channel (xmltv_id, site_id, name, lang)
|
||||||
|
- `date`: The 'dayjs' instance with the requested date
|
||||||
|
- `content`: The response data as a String
|
||||||
|
- `buffer`: The response data as an ArrayBuffer
|
||||||
|
- `headers`: The response headers
|
||||||
|
- `request`: The request config
|
||||||
|
- `cached`: A boolean to check whether this request was cached or not
|
||||||
|
|
||||||
|
### Program Properties
|
||||||
|
|
||||||
|
List of properties that can be assigned to each program during parsing.
|
||||||
|
|
||||||
|
| Property | Aliases | Type | Required |
|
||||||
|
| --------------- | -------------------------------- | ------------------------------------------ | -------- |
|
||||||
|
| start | | `String \| Number \| Date()` | true |
|
||||||
|
| stop | | `String \| Number \| Date()` | true |
|
||||||
|
| title | titles | `String \| Object \| String[] \| Object[]` | true |
|
||||||
|
| subTitle | subTitles, sub_title, sub_titles | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| description | desc, descriptions | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| date | | `String \| Number \| Date()` | false |
|
||||||
|
| category | categories | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| keyword | keywords | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| language | languages | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| origLanguage | origLanguages | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| length | | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| url | urls | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| country | countries | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| video | | `Object` | false |
|
||||||
|
| audio | | `Object` | false |
|
||||||
|
| season | | `String \| Number` | false |
|
||||||
|
| episode | | `String \| Number` | false |
|
||||||
|
| episodeNumber | episodeNum, episodeNumbers | `Object` | false |
|
||||||
|
| previouslyShown | | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| premiere | | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| lastChance | | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| new | | `Boolean` | false |
|
||||||
|
| subtitles | | `Object \| Object[]` | false |
|
||||||
|
| rating | ratings | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| starRating | starRatings | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| review | reviews | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| director | directors | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| actor | actors | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| writer | writers | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| adapter | adapters | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| producer | producers | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| presenter | presenters | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| composer | composers | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| editor | editors | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| commentator | commentators | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| guest | guests | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| image | images | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
| icon | icons | `String \| Object \| String[] \| Object[]` | false |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
start: '2021-03-19T06:00:00.000Z',
|
||||||
|
stop: '2021-03-19T06:30:00.000Z',
|
||||||
|
title: 'Program 1',
|
||||||
|
subTitle: 'Sub-title & 1',
|
||||||
|
description: 'Description for Program 1',
|
||||||
|
date: '2022-05-06',
|
||||||
|
categories: ['Comedy', 'Drama'],
|
||||||
|
keywords: [
|
||||||
|
{ lang: 'en', value: 'physical-comedy' },
|
||||||
|
{ lang: 'en', value: 'romantic' }
|
||||||
|
],
|
||||||
|
language: 'English',
|
||||||
|
origLanguage: { lang: 'en', value: 'French' },
|
||||||
|
length: { units: 'minutes', value: '60' },
|
||||||
|
url: 'http://example.com/title.html',
|
||||||
|
country: 'US',
|
||||||
|
video: {
|
||||||
|
present: 'yes',
|
||||||
|
colour: 'no',
|
||||||
|
aspect: '16:9',
|
||||||
|
quality: 'HDTV'
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
present: 'yes',
|
||||||
|
stereo: 'Dolby Digital'
|
||||||
|
},
|
||||||
|
season: 9,
|
||||||
|
episode: 239,
|
||||||
|
previouslyShown: [{ start: '20080711000000', channel: 'channel-two.tv' }],
|
||||||
|
premiere: 'First time on British TV',
|
||||||
|
lastChance: [{ lang: 'en', value: 'Last time on this channel' }],
|
||||||
|
new: true,
|
||||||
|
subtitles: [
|
||||||
|
{ type: 'teletext', language: 'English' },
|
||||||
|
{ type: 'onscreen', language: [{ lang: 'en', value: 'Spanish' }] }
|
||||||
|
],
|
||||||
|
rating: {
|
||||||
|
system: 'MPAA',
|
||||||
|
value: 'P&G',
|
||||||
|
icon: 'http://example.com/pg_symbol.png'
|
||||||
|
},
|
||||||
|
starRatings: [
|
||||||
|
{
|
||||||
|
system: 'TV Guide',
|
||||||
|
value: '4/5',
|
||||||
|
icon: [{ src: 'stars.png', width: 100, height: 100 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
system: 'IMDB',
|
||||||
|
value: '8/10'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
reviews: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
source: 'Rotten Tomatoes',
|
||||||
|
reviewer: 'Joe Bloggs',
|
||||||
|
lang: 'en',
|
||||||
|
value: 'This is a fantastic show!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
source: 'IDMB',
|
||||||
|
reviewer: 'Jane Doe',
|
||||||
|
lang: 'en',
|
||||||
|
value: 'I love this show!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'url',
|
||||||
|
source: 'Rotten Tomatoes',
|
||||||
|
reviewer: 'Joe Bloggs',
|
||||||
|
lang: 'en',
|
||||||
|
value: 'https://example.com/programme_one_review'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
directors: [
|
||||||
|
{
|
||||||
|
value: 'Director 1',
|
||||||
|
url: { value: 'http://example.com/director1.html', system: 'TestSystem' },
|
||||||
|
image: [
|
||||||
|
'https://example.com/image1.jpg',
|
||||||
|
{
|
||||||
|
value: 'https://example.com/image2.jpg',
|
||||||
|
type: 'person',
|
||||||
|
size: '2',
|
||||||
|
system: 'TestSystem',
|
||||||
|
orient: 'P'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Director 2'
|
||||||
|
],
|
||||||
|
actors: ['Actor 1', 'Actor 2'],
|
||||||
|
writer: 'Writer 1',
|
||||||
|
producers: 'Roger Dobkowitz',
|
||||||
|
presenters: 'Drew Carey',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
type: 'poster',
|
||||||
|
size: '1',
|
||||||
|
orient: 'P',
|
||||||
|
system: 'tvdb',
|
||||||
|
value: 'https://tvdb.com/programme_one_poster_1.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'poster',
|
||||||
|
size: '2',
|
||||||
|
orient: 'P',
|
||||||
|
system: 'tmdb',
|
||||||
|
value: 'https://tmdb.com/programme_one_poster_2.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'backdrop',
|
||||||
|
size: '3',
|
||||||
|
orient: 'L',
|
||||||
|
system: 'tvdb',
|
||||||
|
value: 'https://tvdb.com/programme_one_backdrop_3.jpg'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
icon: 'https://example.com/images/Program1.png?x=шеллы&sid=777'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -59,44 +259,50 @@ More detailed instructions for this file can be found here: https://github.com/f
|
|||||||
With this file we can test the previously created config and make sure it works as you expect.
|
With this file we can test the previously created config and make sure it works as you expect.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { url, parser } = require('./example.com.config.js')
|
const { parser, url } = require('./example.com.config.js')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
const date = dayjs.utc('2022-11-18', 'YYYY-MM-DD').startOf('d')
|
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
|
||||||
const channel = { site_id: 'bbc1', xmltv_id: 'BBCOne.uk', lang: 'en' }
|
const channel = { site_id: 'bbc1', xmltv_id: 'BBCOne.uk' }
|
||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2022-11-18')
|
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can parse response', () => {
|
it('can parse response', () => {
|
||||||
const content = `[{"start":"2022-11-18T01:30:00.000Z","stop":"2022-11-18T02:00:00.000Z","title":"Program 1"}]`
|
const content =
|
||||||
|
'[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]'
|
||||||
|
|
||||||
const results = parser({ content })
|
const results = parser({ content })
|
||||||
|
|
||||||
expect(results).toMatchObject([
|
expect(results.length).toBe(2)
|
||||||
{
|
expect(results[0]).toMatchObject({
|
||||||
start: '2022-11-18T01:30:00.000Z',
|
title: 'Program 1',
|
||||||
stop: '2022-11-18T02:00:00.000Z',
|
start: '2025-01-12T00:00:00.000Z',
|
||||||
title: 'Program 1'
|
stop: '2025-01-12T00:30:00.000Z'
|
||||||
}
|
})
|
||||||
])
|
expect(results[1]).toMatchObject({
|
||||||
|
title: 'Program 2',
|
||||||
|
start: '2025-01-12T00:30:00.000Z',
|
||||||
|
stop: '2025-01-12T01:00:00.000Z'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can handle empty guide', () => {
|
it('can handle empty guide', () => {
|
||||||
const results = parser({ content: '' })
|
const result = parser({
|
||||||
|
date,
|
||||||
|
channel,
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
expect(results).toMatchObject([])
|
expect(result).toMatchObject([])
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
To run the tests you can use the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm test --- example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Detailed documentation for the tests can be found here: https://jestjs.io/docs/using-matchers
|
Detailed documentation for the tests can be found here: https://jestjs.io/docs/using-matchers
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -110,21 +316,89 @@ This file contains a list of channels available at the source.
|
|||||||
```xml
|
```xml
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<channels>
|
<channels>
|
||||||
<channel site="example.com" lang="en" xmltv_id="BBCOne.uk" site_id="bbc1">BBC 1</channel>
|
<channel site="example.com" lang="en" xmltv_id="BBCOne.uk" site_id="bbc1">BBC One</channel>
|
||||||
</channels>
|
</channels>
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
After creating all the files we can make sure that the guide loads correctly and has no errors using the command:
|
<details>
|
||||||
|
<summary>readme.md</summary>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
This file contains instructions on how to use this config.
|
||||||
|
|
||||||
|
````
|
||||||
|
# example.com
|
||||||
|
|
||||||
|
https://example.com
|
||||||
|
|
||||||
|
### Download the guide
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run grab --- --site=example.com
|
npm run grab --- --site=example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
If the download is successful, the `guide.xml` file with the ready to use program should appear in the root directory.
|
### Test
|
||||||
|
|
||||||
After that, all that remains is to commit all the changes and send a pull request.
|
```sh
|
||||||
|
npm test --- example.com
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
The fastest way to create all these files is to use the command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run sites:init --- example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
After you finish working on the files you can make sure that everything works by running the config test:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm test --- example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Then check that all channels have the correct `xmltv-id`:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run channels:validate sites/example.com/example.com.channels.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
And then try downloading the guide itself:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run grab --- example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If everything goes well just [commit](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/about-commits) all changes and send us a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests).
|
||||||
|
|
||||||
|
### How to map the channels?
|
||||||
|
|
||||||
|
In order for the guides to be linked with playlists from [iptv-org/iptv](https://github.com/iptv-org/iptv) and also with our other projects, each channel must have the same ID in the description as in our [iptv-org/database](https://github.com/iptv-org/database).
|
||||||
|
|
||||||
|
To check this, select one of the sites in the [SITES.md](SITES.md), open its `*.channels.xml` file and check that all channels have a valid `xmltv_id`. A list of all channels with corresponding IDs can be found at [iptv-org.github.io](https://iptv-org.github.io/).
|
||||||
|
|
||||||
|
If the channel is not in our database yet, you can add it to it through this [form](https://github.com/iptv-org/database/issues/new?assignees=&labels=channels%3Aadd&projects=&template=1_channels_add.yml&title=Add%3A+).
|
||||||
|
|
||||||
|
If the `*.channels.xml` file contains many channels without `xmltv_id`, you can speed up the process by running the command in the [Console](https://en.wikipedia.org/wiki/Windows_Console) (or [Terminal](<https://en.wikipedia.org/wiki/Terminal_(macOS)>) if you have macOS):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run channels:edit path/to/channels.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
This way, you can map channels by simply selecting the proper ID from the list:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
? Select channel ID for "BBC One" (bbc1): (Use arrow keys)
|
||||||
|
❯ BBCOne.uk (BBC One, BBC1, BBC Television, BBC Television Service)
|
||||||
|
BBCOneHD.uk (BBC One HD)
|
||||||
|
Type...
|
||||||
|
Skip
|
||||||
|
```
|
||||||
|
|
||||||
|
Once complete, [commit](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/about-commits) all changes and send a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests).
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -147,13 +421,16 @@ For scripts to work, you must have [Node.js](https://nodejs.org/en) installed on
|
|||||||
|
|
||||||
To run scripts use the `npm run <script-name>` command.
|
To run scripts use the `npm run <script-name>` command.
|
||||||
|
|
||||||
|
- `act:check`: allows to test the [check](https://github.com/iptv-org/iptv/blob/master/.github/workflows/check.yml) workflow locally. Depends on [nektos/act](https://github.com/nektos/act).
|
||||||
- `act:update`: allows to test the [update](https://github.com/iptv-org/iptv/blob/master/.github/workflows/update.yml) workflow locally. Depends on [nektos/act](https://github.com/nektos/act).
|
- `act:update`: allows to test the [update](https://github.com/iptv-org/iptv/blob/master/.github/workflows/update.yml) workflow locally. Depends on [nektos/act](https://github.com/nektos/act).
|
||||||
- `api:load`: downloads the latest channels data from the [iptv-org/api](https://github.com/iptv-org/api).
|
- `api:load`: downloads the latest channels data from the [iptv-org/api](https://github.com/iptv-org/api).
|
||||||
- `api:generate`: generates a JSON file with all channels for the [iptv-org/api](https://github.com/iptv-org/api) repository.
|
- `api:generate`: generates a JSON file with all channels for the [iptv-org/api](https://github.com/iptv-org/api) repository.
|
||||||
- `channels:lint`: сhecks the channel lists for syntax errors.
|
- `channels:lint`: сhecks the channel lists for syntax errors.
|
||||||
- `channels:parse`: generates a list of channels based on the site configuration.
|
- `channels:parse`: generates a list of channels based on the site configuration.
|
||||||
- `channels:editor`: utility for quick channels markup.
|
- `channels:edit`: utility for quick channels mapping.
|
||||||
- `channels:validate`: checks the description of channels for errors.
|
- `channels:validate`: checks the description of channels for errors.
|
||||||
|
- `sites:init`: creates a new site config from the template.
|
||||||
|
- `sites:update`: updates the list of sites and their status in [SITES.md](SITES.md).
|
||||||
- `grab`: downloads a program from a specified source.
|
- `grab`: downloads a program from a specified source.
|
||||||
- `serve`: starts the [web server](https://github.com/vercel/serve).
|
- `serve`: starts the [web server](https://github.com/vercel/serve).
|
||||||
- `lint`: сhecks the scripts for syntax errors.
|
- `lint`: сhecks the scripts for syntax errors.
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
ARG GIT_REPO=https://github.com/iptv-org/epg.git
|
||||||
|
ARG GIT_BRANCH=master
|
||||||
|
ARG WORKDIR=/epg
|
||||||
|
ENV CRON_SCHEDULE="0 0 * * *"
|
||||||
|
ENV GZIP=false
|
||||||
|
ENV MAX_CONNECTIONS=1
|
||||||
|
ENV DAYS=
|
||||||
|
RUN apk update \
|
||||||
|
&& apk upgrade --available \
|
||||||
|
&& apk add curl git tzdata bash \
|
||||||
|
&& npm install -g npm@latest \
|
||||||
|
&& npm install pm2 -g \
|
||||||
|
&& mkdir $(echo "${WORKDIR}") -p \
|
||||||
|
&& cd $WORKDIR \
|
||||||
|
&& git clone --depth 1 -b $(echo "${GIT_BRANCH} ${GIT_REPO}") . \
|
||||||
|
&& npm install \
|
||||||
|
&& mkdir /public
|
||||||
|
RUN apk del git curl \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
COPY pm2.config.js $WORKDIR
|
||||||
|
WORKDIR $WORKDIR
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD [ "pm2-runtime", "pm2.config.js" ]
|
||||||
@@ -1,174 +1,237 @@
|
|||||||
# EPG
|
# EPG [](https://github.com/iptv-org/epg/actions/workflows/update.yml)
|
||||||
|
|
||||||
Tools for downloading the EPG (Electronic Program Guide) for thousands of TV channels from hundreds of sources.
|
Tools for downloading the EPG (Electronic Program Guide) for thousands of TV channels from hundreds of sources.
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- ✨ [Installation](#installation)
|
- ✨ [Installation](#installation)
|
||||||
- 🚀 [Usage](#usage)
|
- 🚀 [Usage](#usage)
|
||||||
- 💫 [Update](#update)
|
- 💫 [Update](#update)
|
||||||
- 📺 [Playlists](#playlists)
|
- 🐋 [Docker](#docker)
|
||||||
- 🗄 [Database](#database)
|
- 📺 [Playlists](#playlists)
|
||||||
- 👨💻 [API](#api)
|
- 🗄 [Database](#database)
|
||||||
- 📚 [Resources](#resources)
|
- 👨💻 [API](#api)
|
||||||
- 💬 [Discussions](#discussions)
|
- 📚 [Resources](#resources)
|
||||||
- 🛠 [Contribution](#contribution)
|
- 💬 [Discussions](#discussions)
|
||||||
- 📄 [License](#license)
|
- 🛠 [Contribution](#contribution)
|
||||||
|
- 📄 [License](#license)
|
||||||
## Installation
|
|
||||||
|
## Installation
|
||||||
First, you need to install [Node.js](https://nodejs.org/en) on your computer. You will also need to install [Git](https://git-scm.com/downloads) to follow these instructions.
|
|
||||||
|
First, you need to install [Node.js](https://nodejs.org/en) on your computer. You will also need to install [Git](https://git-scm.com/downloads) to follow these instructions.
|
||||||
After that open the [Console](https://en.wikipedia.org/wiki/Windows_Console) (or [Terminal](<https://en.wikipedia.org/wiki/Terminal_(macOS)>) if you have macOS) and type the following command:
|
|
||||||
|
After that open the [Console](https://en.wikipedia.org/wiki/Windows_Console) (or [Terminal](<https://en.wikipedia.org/wiki/Terminal_(macOS)>) if you have macOS) and type the following command:
|
||||||
```sh
|
|
||||||
git clone --depth 1 -b master https://github.com/iptv-org/epg.git
|
```sh
|
||||||
```
|
git clone --depth 1 -b master https://github.com/iptv-org/epg.git
|
||||||
|
```
|
||||||
Then navigate to the downloaded `epg` folder:
|
|
||||||
|
Then navigate to the downloaded `epg` folder:
|
||||||
```sh
|
|
||||||
cd epg
|
```sh
|
||||||
```
|
cd epg
|
||||||
|
```
|
||||||
And install all the dependencies:
|
|
||||||
|
And install all the dependencies:
|
||||||
```sh
|
|
||||||
npm install
|
```sh
|
||||||
```
|
npm install
|
||||||
|
```
|
||||||
## Usage
|
|
||||||
|
## Usage
|
||||||
To start the download of the guide, select one of the [supported sites](SITES.md) and paste its name into the command below:
|
|
||||||
|
To start the download of the guide, select one of the supported sites from [SITES.md](SITES.md) file and paste its name into the command below:
|
||||||
```sh
|
|
||||||
npm run grab --- --site=example.com
|
```sh
|
||||||
```
|
npm run grab --- --site=example.com
|
||||||
|
```
|
||||||
And once the download is complete, the guide will be saved to the `guide.xml` file.
|
|
||||||
|
Then run it and wait for the guide to finish downloading. When finished, a new `guide.xml` file will appear in the current directory.
|
||||||
```sh
|
|
||||||
Usage: npm run grab --- [options]
|
You can also customize the behavior of the script using this options:
|
||||||
|
|
||||||
Options:
|
```sh
|
||||||
-s, --site <name> Name of the site to parse
|
Usage: npm run grab --- [options]
|
||||||
-c, --channels <path> Path to *.channels.xml file (required if the "--site" attribute is
|
|
||||||
not specified)
|
Options:
|
||||||
-o, --output <path> Path to output file (default: "guide.xml")
|
-s, --site <name> Name of the site to parse
|
||||||
-l, --lang <code> Filter channels by language (ISO 639-2 code)
|
-c, --channels <path> Path to *.channels.xml file (required if the "--site" attribute is
|
||||||
-t, --timeout <milliseconds> Override the default timeout for each request
|
not specified)
|
||||||
-d, --delay <milliseconds> Override the default delay between request
|
-o, --output <path> Path to output file (default: "guide.xml")
|
||||||
--days <days> Override the number of days for which the program will be loaded
|
-l, --lang <codes> Allows you to restrict downloading to channels in specified languages only (example: "en,id")
|
||||||
(defaults to the value from the site config)
|
-t, --timeout <milliseconds> Timeout for each request in milliseconds (default: 0)
|
||||||
--maxConnections <number> Limit on the number of concurrent requests (default: 1)
|
-d, --delay <milliseconds> Delay between request in milliseconds (default: 0)
|
||||||
--cron <expression> Schedule a script run (example: "0 0 * * *")
|
-x, --proxy <url> Use the specified proxy (example: "socks5://username:password@127.0.0.1:1234")
|
||||||
--gzip Create a compressed version of the guide as well (default: false)
|
--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)
|
||||||
|
--gzip Specifies whether or not to create a compressed version of the guide (default: false)
|
||||||
### Access the guide by URL
|
--curl Display each request as CURL (default: false)
|
||||||
|
```
|
||||||
You can make the guide available via URL by running your own server:
|
|
||||||
|
### Parallel downloading
|
||||||
```sh
|
|
||||||
npm run serve
|
By default, the guide for each channel is downloaded one by one, but you can change this behavior by increasing the number of simultaneous requests using the `--maxConnections` attribute:
|
||||||
```
|
|
||||||
|
```sh
|
||||||
After that, the guide will be available at the link:
|
npm run grab --- --site=example.com --maxConnections=10
|
||||||
|
```
|
||||||
```
|
|
||||||
http://localhost:3000/guide.xml
|
But be aware that under heavy load, some sites may start return an error or completely block your access.
|
||||||
```
|
|
||||||
|
### Use custom channel list
|
||||||
In addition it will be available to other devices on the same local network at the address:
|
|
||||||
|
Create an XML file and copy the descriptions of all the channels you need from the [/sites](sites) into it:
|
||||||
```
|
|
||||||
http://<your_local_ip_address>:3000/guide.xml
|
```xml
|
||||||
```
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
### Parallel downloading
|
<channel site="arirang.com" lang="en" xmltv_id="ArirangTV.kr" site_id="CH_K">Arirang TV</channel>
|
||||||
|
...
|
||||||
By default, the guide for each channel is downloaded one by one, but you can change this behavior by increasing the number of simultaneous requests using the `--maxConnections` attribute:
|
</channels>
|
||||||
|
```
|
||||||
```sh
|
|
||||||
npm run grab --- --site=example.com --maxConnections=10
|
And then specify the path to that file via the `--channels` attribute:
|
||||||
```
|
|
||||||
|
```sh
|
||||||
But be aware that under heavy load, some sites may start return an error or completely block your access.
|
npm run grab --- --channels=path/to/custom.channels.xml
|
||||||
|
```
|
||||||
### Use custom channel list
|
|
||||||
|
### Run on schedule
|
||||||
Create an XML file and copy the descriptions of all the channels you need from the [/sites](sites) into it:
|
|
||||||
|
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.
|
||||||
```xml
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/):
|
||||||
<channels>
|
|
||||||
<channel site="arirang.com" lang="en" xmltv_id="ArirangTV.kr" site_id="CH_K">Arirang TV</channel>
|
```sh
|
||||||
...
|
npx chronos --execute="npm run grab --- --site=example.com" --pattern="0 0,12 * * *" --log
|
||||||
</channels>
|
```
|
||||||
```
|
|
||||||
|
For more info go to [chronos](https://github.com/freearhey/chronos) documentation.
|
||||||
And then specify the path to that file via the `--channels` attribute:
|
|
||||||
|
### Access the guide by URL
|
||||||
```sh
|
|
||||||
npm run grab --- --channels=path/to/custom.channels.xml
|
You can make the guide available via URL by running your own server. The easiest way to do this is to run this command:
|
||||||
```
|
|
||||||
|
```sh
|
||||||
### Run on schedule
|
npx serve
|
||||||
|
```
|
||||||
If you want to download the guide automatically on a schedule, you need to pass a valid [cron expression](https://crontab.guru/) to the script using the `--cron` attribute:
|
|
||||||
|
After that, the guide will be available at the link:
|
||||||
```sh
|
|
||||||
npm run grab --- --site=example.com --cron="0 0 * * *"
|
```
|
||||||
```
|
http://localhost:3000/guide.xml
|
||||||
|
```
|
||||||
## Update
|
|
||||||
|
In addition it will be available to other devices on the same local network at the address:
|
||||||
If you have downloaded the repository code according to the instructions above, then to update it will be enough to run the command:
|
|
||||||
|
```
|
||||||
```sh
|
http://<your_local_ip_address>:3000/guide.xml
|
||||||
git pull
|
```
|
||||||
```
|
|
||||||
|
For more info go to [serve](https://github.com/vercel/serve) documentation.
|
||||||
And then update all the dependencies:
|
|
||||||
|
## Update
|
||||||
```sh
|
|
||||||
npm install
|
If you have downloaded the repository code according to the instructions above, then to update it will be enough to run the command:
|
||||||
```
|
|
||||||
|
```sh
|
||||||
## Playlists
|
git pull
|
||||||
|
```
|
||||||
Playlists with already linked guides can be found in the [iptv-org/iptv](https://github.com/iptv-org/iptv) repository.
|
|
||||||
|
And then update all the dependencies:
|
||||||
## Database
|
|
||||||
|
```sh
|
||||||
All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there.
|
npm install
|
||||||
|
```
|
||||||
## API
|
|
||||||
|
## Docker
|
||||||
The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository.
|
|
||||||
|
### Build an image
|
||||||
## Resources
|
|
||||||
|
```sh
|
||||||
Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository.
|
docker build -t iptv-org/epg --no-cache .
|
||||||
|
```
|
||||||
## Discussions
|
|
||||||
|
### Create and run container
|
||||||
If you have a question or an idea, you can post it in the [Discussions](https://github.com/orgs/iptv-org/discussions) tab.
|
|
||||||
|
```sh
|
||||||
## Contribution
|
docker run -p 3000:3000 -v /path/to/channels.xml:/epg/channels.xml iptv-org/epg
|
||||||
|
```
|
||||||
Please make sure to read the [Contributing Guide](https://github.com/iptv-org/epg/blob/master/CONTRIBUTING.md) before sending [issue](https://github.com/iptv-org/epg/issues) or a [pull request](https://github.com/iptv-org/epg/pulls).
|
|
||||||
|
By default, the guide will be downloaded every day at 00:00 UTC and saved to the `/epg/public/guide.xml` file inside the container.
|
||||||
And thank you to everyone who has already contributed!
|
|
||||||
|
From the outside, it will be available at this link:
|
||||||
### Backers
|
|
||||||
|
```
|
||||||
<a href="https://opencollective.com/iptv-org"><img src="https://opencollective.com/iptv-org/backers.svg?width=890" /></a>
|
http://localhost:3000/guide.xml
|
||||||
|
```
|
||||||
### Contributors
|
|
||||||
|
or
|
||||||
<a href="https://github.com/iptv-org/epg/graphs/contributors"><img src="https://opencollective.com/iptv-org/contributors.svg?width=890" /></a>
|
|
||||||
|
```
|
||||||
## License
|
http://<your_local_ip_address>:3000/guide.xml
|
||||||
|
```
|
||||||
[](LICENSE)
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
To fine-tune the execution, you can pass environment variables to the container as follows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run \
|
||||||
|
-p 5000:3000 \
|
||||||
|
-v /path/to/channels.xml:/epg/channels.xml \
|
||||||
|
-e CRON_SCHEDULE="0 0,12 * * *" \
|
||||||
|
-e MAX_CONNECTIONS=10 \
|
||||||
|
-e GZIP=true \
|
||||||
|
-e CURL=true \
|
||||||
|
-e PROXY="socks5://127.0.0.1:1234" \
|
||||||
|
-e DAYS=14 \
|
||||||
|
-e TIMEOUT=5 \
|
||||||
|
-e DELAY=2 \
|
||||||
|
iptv-org/epg
|
||||||
|
```
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| --------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| 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) |
|
||||||
|
| 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 |
|
||||||
|
| 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) |
|
||||||
|
| DELAY | Delay between request in milliseconds (default: 0) |
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository.
|
||||||
|
|
||||||
|
## Discussions
|
||||||
|
|
||||||
|
If you have a question or an idea, you can post it in the [Discussions](https://github.com/orgs/iptv-org/discussions) tab.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
Please make sure to read the [Contributing Guide](https://github.com/iptv-org/epg/blob/master/CONTRIBUTING.md) before sending [issue](https://github.com/iptv-org/epg/issues) or a [pull request](https://github.com/iptv-org/epg/pulls).
|
||||||
|
|
||||||
|
And thank you to everyone who has already contributed!
|
||||||
|
|
||||||
|
### Backers
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/iptv-org"><img src="https://opencollective.com/iptv-org/backers.svg?width=890" /></a>
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/iptv-org/epg/graphs/contributors"><img src="https://opencollective.com/iptv-org/contributors.svg?width=890" /></a>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
|||||||
@@ -1,203 +1,242 @@
|
|||||||
# Sites
|
# Sites
|
||||||
|
|
||||||
| Site | Status | Notes |
|
<table>
|
||||||
| ------------------------------------------------------------------ | ------ | ---------------------------------------------------------------------------------------- |
|
<thead>
|
||||||
| [9tv.co.il](sites/9tv.co.il) | 🟢 | |
|
<tr><th align="left">Site</th><th align="left" colspan="2">Channels<br>(total / with xmltv-id)</th><th align="left">Status</th><th align="left">Notes</th></tr>
|
||||||
| [abc.net.au](sites/abc.net.au) | 🟢 | |
|
</thead>
|
||||||
| [allente.dk](sites/allente.dk) | 🟢 | |
|
<tbody>
|
||||||
| [allente.fi](sites/allente.fi) | 🟢 | |
|
<tr><td><a href="sites/9tv.co.il">9tv.co.il</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [allente.no](sites/allente.no) | 🟢 | |
|
<tr><td><a href="sites/abc.net.au">abc.net.au</a></td><td align="right">548</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [allente.se](sites/allente.se) | 🟢 | |
|
<tr><td><a href="sites/allente.dk">allente.dk</a></td><td align="right">74</td><td align="right">43</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [andorradifusio.ad](sites/andorradifusio.ad) | 🟢 | |
|
<tr><td><a href="sites/allente.fi">allente.fi</a></td><td align="right">71</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [anteltv.com.uy](sites/anteltv.com.uy) | 🟢 | |
|
<tr><td><a href="sites/allente.no">allente.no</a></td><td align="right">84</td><td align="right">52</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [arianaafgtv.com](sites/arianaafgtv.com) | 🟢 | |
|
<tr><td><a href="sites/allente.se">allente.se</a></td><td align="right">92</td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [arianatelevision.com](sites/arianatelevision.com) | 🟢 | |
|
<tr><td><a href="sites/andorradifusio.ad">andorradifusio.ad</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [arirang.com](sites/arirang.com) | 🟢 | |
|
<tr><td><a href="sites/anteltv.com.uy">anteltv.com.uy</a></td><td align="right">53</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [artonline.tv](sites/artonline.tv) | 🟢 | |
|
<tr><td><a href="sites/antennaeurope.gr">antennaeurope.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [awilime.com](sites/awilime.com) | 🟢 | |
|
<tr><td><a href="sites/antennapacific.gr">antennapacific.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [bein.com](sites/bein.com) | 🟢 | |
|
<tr><td><a href="sites/antennasatellite.gr">antennasatellite.gr</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [beinsports.com](sites/beinsports.com) | 🟢 | |
|
<tr><td><a href="sites/arianaafgtv.com">arianaafgtv.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [berrymedia.co.kr](sites/berrymedia.co.kr) | 🟢 | |
|
<tr><td><a href="sites/arianatelevision.com">arianatelevision.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [cablego.com.pe](sites/cablego.com.pe) | 🟢 | |
|
<tr><td><a href="sites/arirang.com">arirang.com</a></td><td align="right">3</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [cableplus.com.uy](sites/cableplus.com.uy) | 🟢 | |
|
<tr><td><a href="sites/artonline.tv">artonline.tv</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [canalplus-haiti.com](sites/canalplus-haiti.com) | 🔴 | https://github.com/iptv-org/epg/issues/2237 |
|
<tr><td><a href="sites/awilime.com">awilime.com</a></td><td align="right">111</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [canalplus.com](sites/canalplus.com) | 🟢 | |
|
<tr><td><a href="sites/bein.com">bein.com</a></td><td align="right">160</td><td align="right">160</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [cgates.lt](sites/cgates.lt) | 🟢 | |
|
<tr><td><a href="sites/beinsports.com">beinsports.com</a></td><td align="right">104</td><td align="right">81</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [chada.ma](sites/chada.ma) | 🟢 | |
|
<tr><td><a href="sites/berrymedia.co.kr">berrymedia.co.kr</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [chaines-tv.orange.fr](sites/chaines-tv.orange.fr) | 🟡 | https://github.com/iptv-org/epg/issues/2395 |
|
<tr><td><a href="sites/cableplus.com.uy">cableplus.com.uy</a></td><td align="right">171</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [clickthecity.com](sites/clickthecity.com) | 🟢 | |
|
<tr><td><a href="sites/canalplus.com">canalplus.com</a></td><td align="right">11720</td><td align="right">212</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [comteco.com.bo](sites/comteco.com.bo) | 🔴 | https://github.com/iptv-org/epg/issues/2239 |
|
<tr><td><a href="sites/cgates.lt">cgates.lt</a></td><td align="right">102</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [content.astro.com.my](sites/content.astro.com.my) | 🟢 | |
|
<tr><td><a href="sites/chada.ma">chada.ma</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [cosmote.gr](sites/cosmote.gr) | 🟢 | |
|
<tr><td><a href="sites/chaines-tv.orange.fr">chaines-tv.orange.fr</a></td><td align="right">295</td><td align="right">146</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [cubmu.com](sites/cubmu.com) | 🟢 | |
|
<tr><td><a href="sites/clickthecity.com">clickthecity.com</a></td><td align="right">32</td><td align="right">30</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [dens.tv](sites/dens.tv) | 🟢 | |
|
<tr><td><a href="sites/content.astro.com.my">content.astro.com.my</a></td><td align="right">157</td><td align="right">112</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [digiturk.com.tr](sites/digiturk.com.tr) | 🔴 | https://github.com/iptv-org/epg/issues/2304, https://github.com/iptv-org/epg/issues/2547 |
|
<tr><td><a href="sites/cosmotetv.gr">cosmotetv.gr</a></td><td align="right">108</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [directv.com](sites/directv.com) | 🔴 | https://github.com/iptv-org/epg/issues/2284 |
|
<tr><td><a href="sites/ctc.ru">ctc.ru</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [directv.com.ar](sites/directv.com.ar) | 🔴 | https://github.com/iptv-org/epg/issues/2248 |
|
<tr><td><a href="sites/cubmu.com">cubmu.com</a></td><td align="right">174</td><td align="right">122</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [directv.com.uy](sites/directv.com.uy) | 🟢 | |
|
<tr><td><a href="sites/cyta.com.cy">cyta.com.cy</a></td><td align="right">116</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [dishtv.in](sites/dishtv.in) | 🔴 | https://github.com/iptv-org/epg/issues/2445 |
|
<tr><td><a href="sites/dens.tv">dens.tv</a></td><td align="right">67</td><td align="right">64</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [dsmart.com.tr](sites/dsmart.com.tr) | 🟡 | https://github.com/iptv-org/epg/issues/2303 |
|
<tr><td><a href="sites/derana.lk">derana.lk</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [dstv.com](sites/dstv.com) | 🟢 | |
|
<tr><td><a href="sites/digea.gr">digea.gr</a></td><td align="right">92</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [elcinema.com](sites/elcinema.com) | 🔴 | https://github.com/iptv-org/epg/issues/2541 |
|
<tr><td><a href="sites/digiturk.com.tr">digiturk.com.tr</a></td><td align="right">108</td><td align="right">107</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [ena.skylifetv.co.kr](sites/ena.skylifetv.co.kr) | 🟢 | |
|
<tr><td><a href="sites/directv.com">directv.com</a></td><td align="right">1043</td><td align="right">696</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2284</td></tr>
|
||||||
| [energeek.cl](sites/energeek.cl) | 🟢 | |
|
<tr><td><a href="sites/directv.com.ar">directv.com.ar</a></td><td align="right">412</td><td align="right">229</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2339</td></tr>
|
||||||
| [entertainment.ie](sites/entertainment.ie) | 🟢 | |
|
<tr><td><a href="sites/directv.com.uy">directv.com.uy</a></td><td align="right">143</td><td align="right">142</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [firstmedia.com](sites/firstmedia.com) | 🟢 | |
|
<tr><td><a href="sites/dishtv.in">dishtv.in</a></td><td align="right">448</td><td align="right">89</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [flixed.io](sites/flixed.io) | 🟡 | https://github.com/iptv-org/epg/issues/2543 |
|
<tr><td><a href="sites/dna.fi">dna.fi</a></td><td align="right">122</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [foxsports.com.au](sites/foxsports.com.au) | 🟢 | |
|
<tr><td><a href="sites/dsmart.com.tr">dsmart.com.tr</a></td><td align="right">104</td><td align="right">90</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [foxtel.com.au](sites/foxtel.com.au) | 🟢 | |
|
<tr><td><a href="sites/dstv.com">dstv.com</a></td><td align="right">6983</td><td align="right">181</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [frikanalen.no](sites/frikanalen.no) | 🟢 | |
|
<tr><td><a href="sites/dtv8.net">dtv8.net</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [gatotv.com](sites/gatotv.com) | 🟢 | |
|
<tr><td><a href="sites/elcinema.com">elcinema.com</a></td><td align="right">262</td><td align="right">226</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [getafteritmedia.com](sites/getafteritmedia.com) | 🟢 | |
|
<tr><td><a href="sites/ena.skylifetv.co.kr">ena.skylifetv.co.kr</a></td><td align="right">6</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [guida.tv](sites/guida.tv) | 🟢 | |
|
<tr><td><a href="sites/energeek.cl">energeek.cl</a></td><td align="right">6</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [guidatv.sky.it](sites/guidatv.sky.it) | 🟢 | |
|
<tr><td><a href="sites/entertainment.ie">entertainment.ie</a></td><td align="right">109</td><td align="right">95</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [hd-plus.de](sites/hd-plus.de) | 🔴 | https://github.com/iptv-org/epg/issues/2173 |
|
<tr><td><a href="sites/epg.112114.xyz">epg.112114.xyz</a></td><td align="right">930</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [horizon.tv](sites/horizon.tv) | 🟢 | |
|
<tr><td><a href="sites/epg.iptvx.one">epg.iptvx.one</a></td><td align="right">2862</td><td align="right">747</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [hoy.tv](sites/hoy.tv) | 🟢 | |
|
<tr><td><a href="sites/epg.telemach.ba">epg.telemach.ba</a></td><td align="right">259</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [i.mjh.nz](sites/i.mjh.nz) | 🟢 | |
|
<tr><td><a href="sites/epg.telemach.me">epg.telemach.me</a></td><td align="right">216</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [i24news.tv](sites/i24news.tv) | 🟢 | |
|
<tr><td><a href="sites/epgmaster.com">epgmaster.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [iltalehti.fi](sites/iltalehti.fi) | 🟡 | https://github.com/iptv-org/epg/issues/2396 |
|
<tr><td><a href="sites/epgshare01.online">epgshare01.online</a></td><td align="right">20971</td><td align="right">17</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [indihometv.com](sites/indihometv.com) | 🟢 | |
|
<tr><td><a href="sites/firstmedia.com">firstmedia.com</a></td><td align="right">116</td><td align="right">101</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [ionplustv.com](sites/ionplustv.com) | 🟢 | |
|
<tr><td><a href="sites/foxsports.com.au">foxsports.com.au</a></td><td align="right">7</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [ipko.tv](sites/ipko.tv) | 🟢 | |
|
<tr><td><a href="sites/foxtel.com.au">foxtel.com.au</a></td><td align="right">99</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [kan.org.il](sites/kan.org.il) | 🔴 | https://github.com/iptv-org/epg/issues/2273 |
|
<tr><td><a href="sites/freetv.tv">freetv.tv</a></td><td align="right">7</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [knr.gl](sites/knr.gl) | 🟢 | |
|
<tr><td><a href="sites/freeview.co.uk">freeview.co.uk</a></td><td align="right">171</td><td align="right">100</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [kplus.vn](sites/kplus.vn) | 🔴 | https://github.com/iptv-org/epg/issues/2240 |
|
<tr><td><a href="sites/frikanalen.no">frikanalen.no</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [kvf.fo](sites/kvf.fo) | 🟢 | |
|
<tr><td><a href="sites/galamtv.kz">galamtv.kz</a></td><td align="right">27</td><td align="right">22</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [m.tv.sms.cz](sites/m.tv.sms.cz) | 🔴 | https://github.com/iptv-org/epg/issues/2241 |
|
<tr><td><a href="sites/gatotv.com">gatotv.com</a></td><td align="right">475</td><td align="right">362</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [m.tving.com](sites/m.tving.com) | 🟢 | |
|
<tr><td><a href="sites/getafteritmedia.com">getafteritmedia.com</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [magticom.ge](sites/magticom.ge) | 🟢 | |
|
<tr><td><a href="sites/gigatv.3bbtv.co.th">gigatv.3bbtv.co.th</a></td><td align="right">79</td><td align="right">38</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mako.co.il](sites/mako.co.il) | 🟢 | |
|
<tr><td><a href="sites/guiadetv.com">guiadetv.com</a></td><td align="right">124</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [maxtv.hrvatskitelekom.hr](sites/maxtv.hrvatskitelekom.hr) | 🔴 | https://github.com/iptv-org/epg/issues/2509 |
|
<tr><td><a href="sites/guida.tv">guida.tv</a></td><td align="right">88</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [maxtvgo.mk](sites/maxtvgo.mk) | 🟢 | |
|
<tr><td><a href="sites/guidatv.sky.it">guidatv.sky.it</a></td><td align="right">168</td><td align="right">153</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mediagenie.co.kr](sites/mediagenie.co.kr) | 🟢 | |
|
<tr><td><a href="sites/guidetnt.com">guidetnt.com</a></td><td align="right">69</td><td align="right">69</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mediaklikk.hu](sites/mediaklikk.hu) | 🟢 | |
|
<tr><td><a href="sites/horizon.tv">horizon.tv</a></td><td align="right">184</td><td align="right">172</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mediasetinfinity.mediaset.it](sites/mediasetinfinity.mediaset.it) | 🟢 | |
|
<tr><td><a href="sites/hoy.tv">hoy.tv</a></td><td align="right">3</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [melita.com](sites/melita.com) | 🟢 | |
|
<tr><td><a href="sites/i.mjh.nz">i.mjh.nz</a></td><td align="right">6458</td><td align="right">1489</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [meo.pt](sites/meo.pt) | 🟡 | https://github.com/iptv-org/epg/issues/2446 |
|
<tr><td><a href="sites/i24news.tv">i24news.tv</a></td><td align="right">4</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [meuguia.tv](sites/meuguia.tv) | 🟢 | |
|
<tr><td><a href="sites/iltalehti.fi">iltalehti.fi</a></td><td align="right">142</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mewatch.sg](sites/mewatch.sg) | 🟢 | |
|
<tr><td><a href="sites/indihometv.com">indihometv.com</a></td><td align="right">130</td><td align="right">124</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mi.tv](sites/mi.tv) | 🟢 | |
|
<tr><td><a href="sites/ionplustv.com">ionplustv.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mncvision.id](sites/mncvision.id) | 🟢 | |
|
<tr><td><a href="sites/ipko.tv">ipko.tv</a></td><td align="right">194</td><td align="right">152</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [moji.id](sites/moji.id) | 🟢 | |
|
<tr><td><a href="sites/jiotv.com">jiotv.com</a></td><td align="right">1094</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mon-programme-tv.be](sites/mon-programme-tv.be) | 🟢 | |
|
<tr><td><a href="sites/kan.org.il">kan.org.il</a></td><td align="right">3</td><td align="right">3</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2273</td></tr>
|
||||||
| [movistarplus.es](sites/movistarplus.es) | 🔴 | https://github.com/iptv-org/epg/issues/2498 |
|
<tr><td><a href="sites/knr.gl">knr.gl</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mtel.ba](sites/mtel.ba) | 🟢 | |
|
<tr><td><a href="sites/kvf.fo">kvf.fo</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mts.rs](sites/mts.rs) | 🟢 | |
|
<tr><td><a href="sites/m.tv.sms.cz">m.tv.sms.cz</a></td><td align="right">1027</td><td align="right">450</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mujtvprogram.cz](sites/mujtvprogram.cz) | 🟢 | |
|
<tr><td><a href="sites/m.tving.com">m.tving.com</a></td><td align="right">30</td><td align="right">26</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [musor.tv](sites/musor.tv) | 🟢 | |
|
<tr><td><a href="sites/magticom.ge">magticom.ge</a></td><td align="right">240</td><td align="right">110</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mysky.com.ph](sites/mysky.com.ph) | 🟢 | |
|
<tr><td><a href="sites/mako.co.il">mako.co.il</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mytelly.co.uk](sites/mytelly.co.uk) | 🟢 | |
|
<tr><td><a href="sites/makrodigitaltelevision.com">makrodigitaltelevision.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [mytvsuper.com](sites/mytvsuper.com) | 🟢 | |
|
<tr><td><a href="sites/maxtvgo.mk">maxtvgo.mk</a></td><td align="right">110</td><td align="right">48</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [neo.io](sites/neo.io) | 🟢 | |
|
<tr><td><a href="sites/mediagenie.co.kr">mediagenie.co.kr</a></td><td align="right">5</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [nhkworldpremium.com](sites/nhkworldpremium.com) | 🟢 | |
|
<tr><td><a href="sites/mediaklikk.hu">mediaklikk.hu</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [nhl.com](sites/nhl.com) | 🟢 | |
|
<tr><td><a href="sites/mediasetinfinity.mediaset.it">mediasetinfinity.mediaset.it</a></td><td align="right">13</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [nostv.pt](sites/nostv.pt) | 🟢 | |
|
<tr><td><a href="sites/melita.com">melita.com</a></td><td align="right">127</td><td align="right">111</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [novacyprus.com](sites/novacyprus.com) | 🟢 | |
|
<tr><td><a href="sites/meo.pt">meo.pt</a></td><td align="right">216</td><td align="right">192</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [novasports.gr](sites/novasports.gr) | 🟢 | |
|
<tr><td><a href="sites/meuguia.tv">meuguia.tv</a></td><td align="right">102</td><td align="right">97</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [nowplayer.now.com](sites/nowplayer.now.com) | 🟢 | |
|
<tr><td><a href="sites/mewatch.sg">mewatch.sg</a></td><td align="right">25</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [nuevosiglo.com.uy](sites/nuevosiglo.com.uy) | 🟢 | |
|
<tr><td><a href="sites/mi.tv">mi.tv</a></td><td align="right">2084</td><td align="right">620</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [nzxmltv.com](sites/nzxmltv.com) | 🟢 | |
|
<tr><td><a href="sites/mncvision.id">mncvision.id</a></td><td align="right">276</td><td align="right">223</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [ontvtonight.com](sites/ontvtonight.com) | 🟢 | |
|
<tr><td><a href="sites/moji.id">moji.id</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [osn.com](sites/osn.com) | 🟢 | |
|
<tr><td><a href="sites/mojmaxtv.hrvatskitelekom.hr">mojmaxtv.hrvatskitelekom.hr</a></td><td align="right">243</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [pbsguam.org](sites/pbsguam.org) | 🟢 | |
|
<tr><td><a href="sites/mon-programme-tv.be">mon-programme-tv.be</a></td><td align="right">111</td><td align="right">95</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [pickx.be](sites/pickx.be) | 🟢 | |
|
<tr><td><a href="sites/movistarplus.es">movistarplus.es</a></td><td align="right">178</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [player.ee.co.uk](sites/player.ee.co.uk) | 🟢 | |
|
<tr><td><a href="sites/mtel.ba">mtel.ba</a></td><td align="right">501</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [playtv.unifi.com.my](sites/playtv.unifi.com.my) | 🟢 | |
|
<tr><td><a href="sites/mts.rs">mts.rs</a></td><td align="right">457</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [plex.tv](sites/plex.tv) | 🟢 | |
|
<tr><td><a href="sites/mujtvprogram.cz">mujtvprogram.cz</a></td><td align="right">216</td><td align="right">202</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [programacion-tv.elpais.com](sites/programacion-tv.elpais.com) | 🟢 | |
|
<tr><td><a href="sites/musor.tv">musor.tv</a></td><td align="right">181</td><td align="right">145</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [programacion.tcc.com.uy](sites/programacion.tcc.com.uy) | 🟢 | |
|
<tr><td><a href="sites/mysky.com.ph">mysky.com.ph</a></td><td align="right">115</td><td align="right">43</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [programetv.ro](sites/programetv.ro) | 🟢 | |
|
<tr><td><a href="sites/mytelly.co.uk">mytelly.co.uk</a></td><td align="right">488</td><td align="right">401</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [programme-tv.net](sites/programme-tv.net) | 🟢 | |
|
<tr><td><a href="sites/mytvsuper.com">mytvsuper.com</a></td><td align="right">108</td><td align="right">99</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [programme-tv.vini.pf](sites/programme-tv.vini.pf) | 🟢 | |
|
<tr><td><a href="sites/neo.io">neo.io</a></td><td align="right">337</td><td align="right">241</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [programme.tvb.com](sites/programme.tvb.com) | 🟢 | |
|
<tr><td><a href="sites/nhkworldpremium.com">nhkworldpremium.com</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [programtv.onet.pl](sites/programtv.onet.pl) | 🟢 | |
|
<tr><td><a href="sites/nhl.com">nhl.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [raiplay.it](sites/raiplay.it) | 🟢 | |
|
<tr><td><a href="sites/nostv.pt">nostv.pt</a></td><td align="right">168</td><td align="right">155</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [reportv.com.ar](sites/reportv.com.ar) | 🟢 | |
|
<tr><td><a href="sites/novacyprus.com">novacyprus.com</a></td><td align="right">29</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [rev.bs](sites/rev.bs) | 🔴 | https://github.com/iptv-org/epg/issues/2255 |
|
<tr><td><a href="sites/novasports.gr">novasports.gr</a></td><td align="right">16</td><td align="right">16</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [rotana.net](sites/rotana.net) | 🟢 | |
|
<tr><td><a href="sites/nowplayer.now.com">nowplayer.now.com</a></td><td align="right">288</td><td align="right">229</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [rtb.gov.bn](sites/rtb.gov.bn) | 🔴 | https://github.com/iptv-org/epg/issues/2257 |
|
<tr><td><a href="sites/nuevosiglo.com.uy">nuevosiglo.com.uy</a></td><td align="right">173</td><td align="right">47</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [rthk.hk](sites/rthk.hk) | 🟢 | |
|
<tr><td><a href="sites/nzxmltv.com">nzxmltv.com</a></td><td align="right">532</td><td align="right">118</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [rtmklik.rtm.gov.my](sites/rtmklik.rtm.gov.my) | 🟢 | |
|
<tr><td><a href="sites/ontvtonight.com">ontvtonight.com</a></td><td align="right">5177</td><td align="right">532</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [rtp.pt](sites/rtp.pt) | 🟢 | |
|
<tr><td><a href="sites/opto.sic.pt">opto.sic.pt</a></td><td align="right">4</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [ruv.is](sites/ruv.is) | 🟢 | |
|
<tr><td><a href="sites/orangetv.orange.es">orangetv.orange.es</a></td><td align="right">168</td><td align="right">165</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [s.mxtv.jp](sites/s.mxtv.jp) | 🟢 | |
|
<tr><td><a href="sites/osn.com">osn.com</a></td><td align="right">118</td><td align="right">98</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [sat.tv](sites/sat.tv) | 🟢 | |
|
<tr><td><a href="sites/pbsguam.org">pbsguam.org</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [shahid.mbc.net](sites/shahid.mbc.net) | 🟢 | |
|
<tr><td><a href="sites/pickx.be">pickx.be</a></td><td align="right">404</td><td align="right">391</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [siba.com.co](sites/siba.com.co) | 🟢 | |
|
<tr><td><a href="sites/player.ee.co.uk">player.ee.co.uk</a></td><td align="right">241</td><td align="right">206</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [singtel.com](sites/singtel.com) | 🟢 | |
|
<tr><td><a href="sites/playtv.unifi.com.my">playtv.unifi.com.my</a></td><td align="right">66</td><td align="right">61</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [sjonvarp.is](sites/sjonvarp.is) | 🟢 | |
|
<tr><td><a href="sites/plex.tv">plex.tv</a></td><td align="right">170</td><td align="right">119</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [sky.co.nz](sites/sky.co.nz) | 🟢 | |
|
<tr><td><a href="sites/pluto.tv">pluto.tv</a></td><td align="right">3302</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [sky.com](sites/sky.com) | 🟡 | https://github.com/iptv-org/epg/issues/2501, https://github.com/iptv-org/epg/issues/2516 |
|
<tr><td><a href="sites/programacion-tv.elpais.com">programacion-tv.elpais.com</a></td><td align="right">195</td><td align="right">104</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [sky.de](sites/sky.de) | 🟢 | |
|
<tr><td><a href="sites/programacion.tcc.com.uy">programacion.tcc.com.uy</a></td><td align="right">149</td><td align="right">56</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [skylife.co.kr](sites/skylife.co.kr) | 🟢 | |
|
<tr><td><a href="sites/programetv.ro">programetv.ro</a></td><td align="right">331</td><td align="right">224</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [skyperfecttv.co.jp](sites/skyperfecttv.co.jp) | 🟢 | |
|
<tr><td><a href="sites/programme-tv.net">programme-tv.net</a></td><td align="right">295</td><td align="right">197</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [sporttv.pt](sites/sporttv.pt) | 🟢 | |
|
<tr><td><a href="sites/programme-tv.vini.pf">programme-tv.vini.pf</a></td><td align="right">58</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [starhubtvplus.com](sites/starhubtvplus.com) | 🟢 | |
|
<tr><td><a href="sites/programme.tvb.com">programme.tvb.com</a></td><td align="right">8</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [startimestv.com](sites/startimestv.com) | 🟢 | |
|
<tr><td><a href="sites/programtv.onet.pl">programtv.onet.pl</a></td><td align="right">590</td><td align="right">362</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [streamingtvguides.com](sites/streamingtvguides.com) | 🟢 | |
|
<tr><td><a href="sites/raiplay.it">raiplay.it</a></td><td align="right">17</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [superguidatv.it](sites/superguidatv.it) | 🟢 | |
|
<tr><td><a href="sites/reportv.com.ar">reportv.com.ar</a></td><td align="right">163</td><td align="right">97</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [taiwanplus.com](sites/taiwanplus.com) | 🟢 | |
|
<tr><td><a href="sites/rikstv.no">rikstv.no</a></td><td align="right">80</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tapdmv.com](sites/tapdmv.com) | 🟢 | |
|
<tr><td><a href="sites/rotana.net">rotana.net</a></td><td align="right">32</td><td align="right">28</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [telenet.tv](sites/telenet.tv) | 🟢 | |
|
<tr><td><a href="sites/rtb.gov.bn">rtb.gov.bn</a></td><td align="right">3</td><td align="right">3</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2257</td></tr>
|
||||||
| [teliatv.ee](sites/teliatv.ee) | 🟢 | |
|
<tr><td><a href="sites/rthk.hk">rthk.hk</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [telkussa.fi](sites/telkussa.fi) | 🟢 | |
|
<tr><td><a href="sites/rtmklik.rtm.gov.my">rtmklik.rtm.gov.my</a></td><td align="right">8</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [telsu.fi](sites/telsu.fi) | 🟢 | |
|
<tr><td><a href="sites/rtp.pt">rtp.pt</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tivu.tv](sites/tivu.tv) | 🟢 | |
|
<tr><td><a href="sites/ruv.is">ruv.is</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [toonamiaftermath.com](sites/toonamiaftermath.com) | 🟢 | |
|
<tr><td><a href="sites/s.mxtv.jp">s.mxtv.jp</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [turksatkablo.com.tr](sites/turksatkablo.com.tr) | 🟢 | |
|
<tr><td><a href="sites/sat.tv">sat.tv</a></td><td align="right">30308</td><td align="right">249</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv-programme.telecablesat.fr](sites/tv-programme.telecablesat.fr) | 🟢 | |
|
<tr><td><a href="sites/shahid.mbc.net">shahid.mbc.net</a></td><td align="right">231</td><td align="right">165</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.blue.ch](sites/tv.blue.ch) | 🟢 | |
|
<tr><td><a href="sites/siba.com.co">siba.com.co</a></td><td align="right">98</td><td align="right">96</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.cctv.com](sites/tv.cctv.com) | 🟢 | |
|
<tr><td><a href="sites/singtel.com">singtel.com</a></td><td align="right">155</td><td align="right">113</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.dir.bg](sites/tv.dir.bg) | 🟢 | |
|
<tr><td><a href="sites/sjonvarp.is">sjonvarp.is</a></td><td align="right">13</td><td align="right">13</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.lv](sites/tv.lv) | 🟢 | |
|
<tr><td><a href="sites/sky.co.nz">sky.co.nz</a></td><td align="right">111</td><td align="right">93</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.magenta.at](sites/tv.magenta.at) | 🟢 | |
|
<tr><td><a href="sites/sky.com">sky.com</a></td><td align="right">559</td><td align="right">458</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2763</td></tr>
|
||||||
| [tv.mail.ru](sites/tv.mail.ru) | 🟢 | |
|
<tr><td><a href="sites/sky.de">sky.de</a></td><td align="right">75</td><td align="right">75</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.movistar.com.pe](sites/tv.movistar.com.pe) | 🟢 | |
|
<tr><td><a href="sites/skylife.co.kr">skylife.co.kr</a></td><td align="right">251</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.nu](sites/tv.nu) | 🟢 | |
|
<tr><td><a href="sites/skyperfectv.co.jp">skyperfectv.co.jp</a></td><td align="right">137</td><td align="right">130</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.post.lu](sites/tv.post.lu) | 🟢 | |
|
<tr><td><a href="sites/snrt.ma">snrt.ma</a></td><td align="right">11</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.trueid.net](sites/tv.trueid.net) | 🟢 | |
|
<tr><td><a href="sites/sporttv.pt">sporttv.pt</a></td><td align="right">9</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.yandex.ru](sites/tv.yandex.ru) | 🟢 | |
|
<tr><td><a href="sites/starhubtvplus.com">starhubtvplus.com</a></td><td align="right">232</td><td align="right">208</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv.yettel.hu](sites/tv.yettel.hu) | 🔴 | https://github.com/iptv-org/epg/issues/2263 |
|
<tr><td><a href="sites/startimestv.com">startimestv.com</a></td><td align="right">77</td><td align="right">58</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv24.co.uk](sites/tv24.co.uk) | 🟢 | |
|
<tr><td><a href="sites/stod2.is">stod2.is</a></td><td align="right">12</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv24.se](sites/tv24.se) | 🟢 | |
|
<tr><td><a href="sites/streamingtvguides.com">streamingtvguides.com</a></td><td align="right">3066</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tv2go.t-2.net](sites/tv2go.t-2.net) | 🟢 | |
|
<tr><td><a href="sites/superguidatv.it">superguidatv.it</a></td><td align="right">204</td><td align="right">163</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tva.tv](sites/tva.tv) | 🔴 | https://github.com/iptv-org/epg/issues/2264 |
|
<tr><td><a href="sites/taiwanplus.com">taiwanplus.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvarenasport.com](sites/tvarenasport.com) | 🟢 | |
|
<tr><td><a href="sites/tapdmv.com">tapdmv.com</a></td><td align="right">39</td><td align="right">7</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvarenasport.hr](sites/tvarenasport.hr) | 🟢 | |
|
<tr><td><a href="sites/tataplay.com">tataplay.com</a></td><td align="right">785</td><td align="right">401</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvcesoir.fr](sites/tvcesoir.fr) | 🟢 | |
|
<tr><td><a href="sites/telebilbao.es">telebilbao.es</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvcubana.icrt.cu](sites/tvcubana.icrt.cu) | 🟢 | |
|
<tr><td><a href="sites/teleboy.ch">teleboy.ch</a></td><td align="right">325</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvgids.nl](sites/tvgids.nl) | 🟡 | https://github.com/iptv-org/epg/issues/2400 |
|
<tr><td><a href="sites/telenet.tv">telenet.tv</a></td><td align="right">260</td><td align="right">91</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvguide.com](sites/tvguide.com) | 🔴 | https://github.com/iptv-org/epg/issues/2542 |
|
<tr><td><a href="sites/teliatv.ee">teliatv.ee</a></td><td align="right">342</td><td align="right">233</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvguide.myjcom.jp](sites/tvguide.myjcom.jp) | 🟢 | |
|
<tr><td><a href="sites/telkussa.fi">telkussa.fi</a></td><td align="right">66</td><td align="right">32</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvhebdo.com](sites/tvhebdo.com) | 🟢 | |
|
<tr><td><a href="sites/telsu.fi">telsu.fi</a></td><td align="right">17</td><td align="right">15</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvheute.at](sites/tvheute.at) | 🟢 | |
|
<tr><td><a href="sites/thesportplus.com">thesportplus.com</a></td><td align="right">3</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvim.tv](sites/tvim.tv) | 🟢 | |
|
<tr><td><a href="sites/tivie.id">tivie.id</a></td><td align="right">45</td><td align="right">44</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvireland.ie](sites/tvireland.ie) | 🟢 | |
|
<tr><td><a href="sites/tivu.tv">tivu.tv</a></td><td align="right">69</td><td align="right">66</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvmi.mt](sites/tvmi.mt) | 🟢 | |
|
<tr><td><a href="sites/toonamiaftermath.com">toonamiaftermath.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvmusor.hu](sites/tvmusor.hu) | 🟢 | |
|
<tr><td><a href="sites/turksatkablo.com.tr">turksatkablo.com.tr</a></td><td align="right">177</td><td align="right">118</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvpassport.com](sites/tvpassport.com) | 🟢 | |
|
<tr><td><a href="sites/tv-programme.telecablesat.fr">tv-programme.telecablesat.fr</a></td><td align="right">268</td><td align="right">250</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvplus.com.tr](sites/tvplus.com.tr) | 🟢 | |
|
<tr><td><a href="sites/tv-spored.siol.net">tv-spored.siol.net</a></td><td align="right">312</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvprofil.com](sites/tvprofil.com) | 🟡 | https://github.com/iptv-org/epg/issues/2399 |
|
<tr><td><a href="sites/tv.blue.ch">tv.blue.ch</a></td><td align="right">1030</td><td align="right">565</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [tvtv.us](sites/tvtv.us) | 🟢 | |
|
<tr><td><a href="sites/tv.cctv.com">tv.cctv.com</a></td><td align="right">94</td><td align="right">88</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [v3.myafn.dodmedia.osd.mil](sites/v3.myafn.dodmedia.osd.mil) | 🟢 | |
|
<tr><td><a href="sites/tv.dir.bg">tv.dir.bg</a></td><td align="right">111</td><td align="right">93</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2779</td></tr>
|
||||||
| [vidio.com](sites/vidio.com) | 🟢 | |
|
<tr><td><a href="sites/tv.lv">tv.lv</a></td><td align="right">137</td><td align="right">49</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [virginmediatelevision.ie](sites/virginmediatelevision.ie) | 🟢 | |
|
<tr><td><a href="sites/tv.magenta.at">tv.magenta.at</a></td><td align="right">307</td><td align="right">228</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [virgintvgo.virginmedia.com](sites/virgintvgo.virginmedia.com) | 🟢 | |
|
<tr><td><a href="sites/tv.mail.ru">tv.mail.ru</a></td><td align="right">664</td><td align="right">643</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [visionplus.id](sites/visionplus.id) | 🟢 | |
|
<tr><td><a href="sites/tv.movistar.com.pe">tv.movistar.com.pe</a></td><td align="right">282</td><td align="right">40</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [vivacom.bg](sites/vivacom.bg) | 🔴 | https://github.com/iptv-org/epg/issues/2270 |
|
<tr><td><a href="sites/tv.nu">tv.nu</a></td><td align="right">199</td><td align="right">180</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [vtm.be](sites/vtm.be) | 🟢 | |
|
<tr><td><a href="sites/tv.post.lu">tv.post.lu</a></td><td align="right">332</td><td align="right">242</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [walesi.com.fj](sites/walesi.com.fj) | 🟢 | |
|
<tr><td><a href="sites/tv.sfr.fr">tv.sfr.fr</a></td><td align="right">489</td><td align="right">456</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [watch.sportsnet.ca](sites/watch.sportsnet.ca) | 🟢 | |
|
<tr><td><a href="sites/tv.trueid.net">tv.trueid.net</a></td><td align="right">266</td><td align="right">74</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [watchyour.tv](sites/watchyour.tv) | 🟢 | |
|
<tr><td><a href="sites/tv.yandex.ru">tv.yandex.ru</a></td><td align="right">97</td><td align="right">67</td><td align="center">🔴</td><td>https://github.com/iptv-org/epg/issues/2803</td></tr>
|
||||||
| [wavve.com](sites/wavve.com) | 🟢 | |
|
<tr><td><a href="sites/tv24.co.uk">tv24.co.uk</a></td><td align="right">1072</td><td align="right">39</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [web.magentatv.de](sites/web.magentatv.de) | 🟢 | |
|
<tr><td><a href="sites/tv24.se">tv24.se</a></td><td align="right">326</td><td align="right">157</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [webtv.delta.nl](sites/webtv.delta.nl) | 🟢 | |
|
<tr><td><a href="sites/tv2go.t-2.net">tv2go.t-2.net</a></td><td align="right">335</td><td align="right">254</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [winplay.co](sites/winplay.co) | 🟢 | |
|
<tr><td><a href="sites/tvarenasport.com">tvarenasport.com</a></td><td align="right">14</td><td align="right">12</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [worldfishingnetwork.com](sites/worldfishingnetwork.com) | 🟢 | |
|
<tr><td><a href="sites/tvarenasport.hr">tvarenasport.hr</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [www3.nhk.or.jp](sites/www3.nhk.or.jp) | 🟢 | |
|
<tr><td><a href="sites/tvcesoir.fr">tvcesoir.fr</a></td><td align="right">135</td><td align="right">133</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [xumo.tv](sites/xumo.tv) | 🟢 | |
|
<tr><td><a href="sites/tvcubana.icrt.cu">tvcubana.icrt.cu</a></td><td align="right">10</td><td align="right">10</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [zap.co.ao](sites/zap.co.ao) | 🟢 | |
|
<tr><td><a href="sites/tvgids.nl">tvgids.nl</a></td><td align="right">115</td><td align="right">90</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [ziggogo.tv](sites/ziggogo.tv) | 🟡 | https://github.com/iptv-org/epg/issues/2393 |
|
<tr><td><a href="sites/tvguide.com">tvguide.com</a></td><td align="right">153</td><td align="right">149</td><td align="center">🟡</td><td>https://github.com/iptv-org/epg/issues/2644</td></tr>
|
||||||
| [znbc.co.zm](sites/znbc.co.zm) | 🟢 | |
|
<tr><td><a href="sites/tvguide.myjcom.jp">tvguide.myjcom.jp</a></td><td align="right">145</td><td align="right">140</td><td align="center">🟢</td><td></td></tr>
|
||||||
| [zuragt.mn](sites/zuragt.mn) | 🟢 | |
|
<tr><td><a href="sites/tvhebdo.com">tvhebdo.com</a></td><td align="right">317</td><td align="right">215</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvheute.at">tvheute.at</a></td><td align="right">53</td><td align="right">53</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvi.iol.pt">tvi.iol.pt</a></td><td align="right">6</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvim.tv">tvim.tv</a></td><td align="right">25</td><td align="right">19</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvinsider.com">tvinsider.com</a></td><td align="right">374</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvireland.ie">tvireland.ie</a></td><td align="right">334</td><td align="right">304</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvkaista.org">tvkaista.org</a></td><td align="right">149</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvmi.mt">tvmi.mt</a></td><td align="right">3</td><td align="right">3</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvmusor.hu">tvmusor.hu</a></td><td align="right">99</td><td align="right">67</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvmustra.hu">tvmustra.hu</a></td><td align="right">188</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvpassport.com">tvpassport.com</a></td><td align="right">19287</td><td align="right">2509</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvplus.com.tr">tvplus.com.tr</a></td><td align="right">150</td><td align="right">144</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvprofil.com">tvprofil.com</a></td><td align="right">5836</td><td align="right">455</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/tvtv.us">tvtv.us</a></td><td align="right">2299</td><td align="right">2255</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/v3.myafn.dodmedia.osd.mil">v3.myafn.dodmedia.osd.mil</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/vidio.com">vidio.com</a></td><td align="right">57</td><td align="right">52</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/virginmediatelevision.ie">virginmediatelevision.ie</a></td><td align="right">5</td><td align="right">5</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/virgintvgo.virginmedia.com">virgintvgo.virginmedia.com</a></td><td align="right">238</td><td align="right">195</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/visionplus.id">visionplus.id</a></td><td align="right">250</td><td align="right">226</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/vivoplay.com.br">vivoplay.com.br</a></td><td align="right">389</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/vtm.be">vtm.be</a></td><td align="right">7</td><td align="right">6</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/walesi.com.fj">walesi.com.fj</a></td><td align="right">9</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/watch.sportsnet.ca">watch.sportsnet.ca</a></td><td align="right">8</td><td align="right">8</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/watchyour.tv">watchyour.tv</a></td><td align="right">40</td><td align="right">24</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/wavve.com">wavve.com</a></td><td align="right">77</td><td align="right">76</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/web.magentatv.de">web.magentatv.de</a></td><td align="right">348</td><td align="right">247</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/webtv.delta.nl">webtv.delta.nl</a></td><td align="right">247</td><td align="right">218</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/winplay.co">winplay.co</a></td><td align="right">2</td><td align="right">2</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/worldfishingnetwork.com">worldfishingnetwork.com</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/www3.nhk.or.jp">www3.nhk.or.jp</a></td><td align="right">1</td><td align="right">1</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/xem.kplus.vn">xem.kplus.vn</a></td><td align="right">77</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/xumo.tv">xumo.tv</a></td><td align="right">350</td><td align="right">33</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/yes.co.il">yes.co.il</a></td><td align="right">174</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/zap.co.ao">zap.co.ao</a></td><td align="right">114</td><td align="right">64</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/zap2it.com">zap2it.com</a></td><td align="right">595</td><td align="right">0</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/ziggogo.tv">ziggogo.tv</a></td><td align="right">152</td><td align="right">130</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/znbc.co.zm">znbc.co.zm</a></td><td align="right">4</td><td align="right">4</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
<tr><td><a href="sites/zuragt.mn">zuragt.mn</a></td><td align="right">36</td><td align="right">25</td><td align="center">🟢</td><td></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||||
|
import stylistic from '@stylistic/eslint-plugin'
|
||||||
|
import globals from 'globals'
|
||||||
|
import tsParser from '@typescript-eslint/parser'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all
|
||||||
|
})
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/strict', 'plugin:@typescript-eslint/stylistic', 'prettier'),
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': typescriptEslint,
|
||||||
|
'@stylistic': stylistic
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest
|
||||||
|
},
|
||||||
|
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'@stylistic/linebreak-style': ['error', 'windows'],
|
||||||
|
|
||||||
|
quotes: [
|
||||||
|
'error',
|
||||||
|
'single',
|
||||||
|
{
|
||||||
|
avoidEscape: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
semi: ['error', 'never']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['tests/__data__/']
|
||||||
|
}
|
||||||
|
]
|
||||||
Generated
+10069
-6660
File diff suppressed because it is too large
Load Diff
+96
-64
@@ -1,91 +1,123 @@
|
|||||||
{
|
{
|
||||||
"name": "epg",
|
"name": "epg",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"act:check": "act pull_request -W .github/workflows/check.yml",
|
||||||
"act:update": "act workflow_dispatch -W .github/workflows/update.yml",
|
"act:update": "act workflow_dispatch -W .github/workflows/update.yml",
|
||||||
"api:load": "npx tsx scripts/commands/api/load.ts",
|
"api:load": "tsx scripts/commands/api/load.ts",
|
||||||
"api:generate": "npx tsx scripts/commands/api/generate.ts",
|
"api:generate": "tsx scripts/commands/api/generate.ts",
|
||||||
"channels:lint": "npx tsx scripts/commands/channels/lint.ts",
|
"channels:lint": "tsx scripts/commands/channels/lint.mts",
|
||||||
"channels:parse": "npx tsx scripts/commands/channels/parse.ts",
|
"channels:parse": "tsx scripts/commands/channels/parse.ts",
|
||||||
"channels:editor": "npx tsx scripts/commands/channels/editor.ts",
|
"channels:edit": "tsx scripts/commands/channels/edit.ts",
|
||||||
"channels:validate": "npx tsx scripts/commands/channels/validate.ts",
|
"channels:validate": "tsx scripts/commands/channels/validate.ts",
|
||||||
"grab": "npx tsx scripts/commands/epg/grab.ts",
|
"sites:init": "tsx scripts/commands/sites/init.ts",
|
||||||
"serve": "npx serve",
|
"sites:update": "tsx scripts/commands/sites/update.ts",
|
||||||
"lint": "npx eslint \"{scripts,tests}/**/*.{ts,js}\"",
|
"grab": "tsx scripts/commands/epg/grab.ts",
|
||||||
"test": "run-script-os",
|
"lint": "npx eslint \"{scripts,tests,sites}/**/*.{ts,mts,js}\"",
|
||||||
"test:win32": "SET \"TZ=Pacific/Nauru\" && npx jest --runInBand",
|
"test": "cross-env TZ=Pacific/Nauru npx jest --runInBand",
|
||||||
"test:default": "TZ=Pacific/Nauru npx jest --runInBand",
|
"postinstall": "skip-postinstall || npm run api:load",
|
||||||
"postinstall": "npm run api:load"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Arhey",
|
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"setupFiles": [
|
||||||
|
"<rootDir>/node_modules/jest-offline"
|
||||||
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(ts|js)$": "ts-jest"
|
"^.+\\.(ts|js)$": "@swc/jest"
|
||||||
},
|
},
|
||||||
"testRegex": "(tests|sites)/(.*?/)?.*test.(js|ts)$",
|
"testMatch": [
|
||||||
"testTimeout": 10000
|
"<rootDir>/sites/**/*.test.(js|ts)",
|
||||||
|
"<rootDir>/tests/commands/**/*.test.(js|ts)"
|
||||||
|
],
|
||||||
|
"testTimeout": 10000,
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"<rootDir>/node_modules/(?!parse-duration/.*|@freearhey/core/.*|glob/.*|srcset/.*|balanced-match/.*|minimatch/.*)"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alex_neo/jest-expect-message": "^1.0.5",
|
"@alex_neo/jest-expect-message": "^1.0.5",
|
||||||
"@freearhey/core": "^0.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@freearhey/search-js": "^0.1.1",
|
"@eslint/js": "^9.32.0",
|
||||||
"@ntlab/sfetch": "^1.0.0",
|
"@freearhey/chronos": "^0.0.1",
|
||||||
"@octokit/core": "^4.1.0",
|
"@freearhey/core": "^0.10.2",
|
||||||
"@types/cli-progress": "^3.11.3",
|
"@freearhey/search-js": "^0.1.2",
|
||||||
"@types/fs-extra": "^11.0.2",
|
"@ntlab/sfetch": "^1.2.0",
|
||||||
"@types/inquirer": "^9.0.3",
|
"@octokit/core": "^7.0.3",
|
||||||
"@types/jest": "^29.5.5",
|
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||||
"@types/lodash": "^4.14.199",
|
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
|
||||||
"@types/node-cleanup": "^2.1.2",
|
"@stylistic/eslint-plugin": "^5.2.2",
|
||||||
"@types/numeral": "^2.0.3",
|
"@swc/core": "^1.13.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
"@swc/jest": "^0.2.39",
|
||||||
"axios": "^1.5.1",
|
"@types/cli-progress": "^3.11.6",
|
||||||
"axios-cookiejar-support": "^5.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"chalk": "^4.1.2",
|
"@types/inquirer": "^9.0.8",
|
||||||
"cheerio": "^1.0.0-rc.10",
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/langs": "^2.0.5",
|
||||||
|
"@types/lodash.orderby": "^4.6.9",
|
||||||
|
"@types/lodash.sortby": "^4.7.9",
|
||||||
|
"@types/lodash.startcase": "^4.4.9",
|
||||||
|
"@types/lodash.uniqby": "^4.7.9",
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
|
"@types/node-cleanup": "^2.1.5",
|
||||||
|
"@types/numeral": "^2.0.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||||
|
"@typescript-eslint/parser": "^8.38.0",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"axios-cookiejar-support": "^6.0.4",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
|
"cheerio": "^1.1.2",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"commander": "^8.2.0",
|
"commander": "^14.0.0",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.4.2",
|
||||||
"cron": "^2.4.3",
|
"cross-env": "^10.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.2.0",
|
||||||
"cwait": "^1.1.2",
|
"cwait": "^1.1.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.13",
|
||||||
"epg-grabber": "^0.37.4",
|
"epg-grabber": "^0.41.0",
|
||||||
"epg-parser": "^0.3.1",
|
"epg-parser": "^0.3.1",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.4",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^11.3.0",
|
||||||
"glob": "^7.2.0",
|
"glob": "^11.0.3",
|
||||||
"iconv-lite": "^0.4.24",
|
"globals": "^16.3.0",
|
||||||
"inquirer": "^8.2.6",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"inquirer": "^12.8.2",
|
||||||
|
"jest": "^30.0.5",
|
||||||
|
"jest-offline": "^1.0.1",
|
||||||
"langs": "^2.0.0",
|
"langs": "^2.0.0",
|
||||||
"libxmljs2": "^0.35.0",
|
"libxml2-wasm": "^0.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash.orderby": "^4.6.0",
|
||||||
"luxon": "^3.3.0",
|
"lodash.sortby": "^4.7.0",
|
||||||
"markdown-include": "^0.4.3",
|
"lodash.startcase": "^4.4.0",
|
||||||
|
"lodash.uniqby": "^4.7.0",
|
||||||
|
"luxon": "^3.7.1",
|
||||||
"mockdate": "^3.0.5",
|
"mockdate": "^3.0.5",
|
||||||
"nedb-promises": "^6.0.3",
|
"nedb-promises": "^6.2.3",
|
||||||
"node-cleanup": "^2.1.2",
|
"node-cleanup": "^2.1.2",
|
||||||
"node-gzip": "^1.1.2",
|
"node-gzip": "^1.1.2",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"parse-duration": "^1.0.0",
|
"pako": "^2.1.0",
|
||||||
|
"parse-duration": "^2.1.4",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
|
"pm2": "^6.0.8",
|
||||||
|
"readline": "^1.3.0",
|
||||||
|
"run-script-os": "^1.1.6",
|
||||||
"serve": "^14.2.4",
|
"serve": "^14.2.4",
|
||||||
"signale": "^1.4.0",
|
"signale": "^1.4.0",
|
||||||
"srcset": "^4.0.0",
|
"skip-postinstall": "^1.0.0",
|
||||||
|
"socks-proxy-agent": "^8.0.5",
|
||||||
|
"srcset": "^5.0.1",
|
||||||
"table2array": "^0.0.2",
|
"table2array": "^0.0.2",
|
||||||
"tabletojson": "^2.0.7",
|
"tabletojson": "^4.1.6",
|
||||||
"tough-cookie": "^5.0.0",
|
"tough-cookie": "^5.1.2",
|
||||||
"transliteration": "^2.2.0",
|
"transliteration": "^2.3.5",
|
||||||
"ts-jest": "^29.1.1",
|
"tsx": "^4.20.3",
|
||||||
"tsx": "^4.19.2",
|
"typescript": "^5.8.3",
|
||||||
"unzipit": "^1.4.0",
|
"unzipit": "^1.4.3",
|
||||||
"wildcard-match": "^5.1.2"
|
"uuid": "^11.1.0",
|
||||||
},
|
"wildcard-match": "^5.1.4"
|
||||||
"devDependencies": {
|
|
||||||
"run-script-os": "^1.1.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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 = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'serve',
|
||||||
|
script: 'npx serve -- public',
|
||||||
|
instances: 1,
|
||||||
|
watch: false,
|
||||||
|
autorestart: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grab',
|
||||||
|
script: `npx chronos -e "${grab}" -p "${process.env.CRON_SCHEDULE}" -l`,
|
||||||
|
instances: 1,
|
||||||
|
watch: false,
|
||||||
|
autorestart: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,16 +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 { Channel } from 'epg-grabber'
|
import { GuideChannel } from '../../models'
|
||||||
|
import { ChannelsParser } from '../../core'
|
||||||
type OutputItem = {
|
import epgGrabber from 'epg-grabber'
|
||||||
channel: string | null
|
import path from 'path'
|
||||||
site: string
|
|
||||||
site_id: string
|
|
||||||
site_name: string
|
|
||||||
lang: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
@@ -19,27 +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: Channel): OutputItem => {
|
const output = channels.map((channel: GuideChannel) => channel.toJSON())
|
||||||
return {
|
|
||||||
channel: channel.xmltv_id || 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'
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { Logger } from '@freearhey/core'
|
import { DATA_DIR } from '../../constants'
|
||||||
import { ApiClient } from '../../core'
|
import { Storage } from '@freearhey/core'
|
||||||
|
import { DataLoader } from '../../core'
|
||||||
async function main() {
|
|
||||||
const logger = new Logger()
|
async function main() {
|
||||||
const client = new ApiClient({ logger })
|
const storage = new Storage(DATA_DIR)
|
||||||
|
const loader = new DataLoader({ storage })
|
||||||
const requests = [
|
|
||||||
client.download('channels.json'),
|
await Promise.all([
|
||||||
client.download('countries.json'),
|
loader.download('blocklist.json'),
|
||||||
client.download('regions.json'),
|
loader.download('categories.json'),
|
||||||
client.download('subdivisions.json')
|
loader.download('channels.json'),
|
||||||
]
|
loader.download('countries.json'),
|
||||||
|
loader.download('languages.json'),
|
||||||
await Promise.all(requests)
|
loader.download('regions.json'),
|
||||||
}
|
loader.download('subdivisions.json'),
|
||||||
|
loader.download('feeds.json'),
|
||||||
main()
|
loader.download('timezones.json'),
|
||||||
|
loader.download('guides.json'),
|
||||||
|
loader.download('streams.json'),
|
||||||
|
loader.download('logos.json')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/replace.ts
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
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 { ChannelsParser } from '../../core'
|
||||||
|
import { DATA_DIR } from '../../constants'
|
||||||
|
import nodeCleanup from 'node-cleanup'
|
||||||
|
import sjs from '@freearhey/search-js'
|
||||||
|
import epgGrabber from 'epg-grabber'
|
||||||
|
import { Command } from 'commander'
|
||||||
|
import readline from 'readline'
|
||||||
|
|
||||||
|
interface ChoiceValue { type: string; value?: Feed | Channel }
|
||||||
|
interface Choice { name: string; short?: string; value: ChoiceValue; default?: boolean }
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
readline
|
||||||
|
.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
})
|
||||||
|
.on('SIGINT', function () {
|
||||||
|
process.emit('SIGINT')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = new Command()
|
||||||
|
|
||||||
|
program.argument('<filepath>', 'Path to *.channels.xml file to edit').parse(process.argv)
|
||||||
|
|
||||||
|
const filepath = program.args[0]
|
||||||
|
const logger = new Logger()
|
||||||
|
const storage = new Storage()
|
||||||
|
let channelList = new ChannelList({ channels: [] })
|
||||||
|
|
||||||
|
main(filepath)
|
||||||
|
nodeCleanup(() => {
|
||||||
|
save(filepath, channelList)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default async function main(filepath: string) {
|
||||||
|
if (!(await storage.exists(filepath))) {
|
||||||
|
throw new Error(`File "${filepath}" does not exists`)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('loading data from api...')
|
||||||
|
const processor = new DataProcessor()
|
||||||
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
|
const data: DataLoaderData = await loader.load()
|
||||||
|
const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData =
|
||||||
|
processor.process(data)
|
||||||
|
|
||||||
|
logger.info('loading channels...')
|
||||||
|
const parser = new ChannelsParser({ storage })
|
||||||
|
channelList = await parser.parse(filepath)
|
||||||
|
const parsedChannelsWithoutId = channelList.channels.filter(
|
||||||
|
(channel: epgGrabber.Channel) => !channel.xmltv_id
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`found ${channelList.channels.count()} channels (including ${parsedChannelsWithoutId.count()} without ID)`
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info('creating search index...')
|
||||||
|
const items = channels.map((channel: Channel) => channel.getSearchable()).all()
|
||||||
|
const searchIndex = sjs.createIndex(items, {
|
||||||
|
searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames']
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info('starting...\n')
|
||||||
|
|
||||||
|
for (const channel of parsedChannelsWithoutId.all()) {
|
||||||
|
try {
|
||||||
|
channel.xmltv_id = await selectChannel(
|
||||||
|
channel,
|
||||||
|
searchIndex,
|
||||||
|
feedsGroupedByChannelId,
|
||||||
|
channelsKeyById
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
logger.info(err.message)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedChannelsWithoutId.forEach((channel: epgGrabber.Channel) => {
|
||||||
|
if (channel.xmltv_id === '-') {
|
||||||
|
channel.xmltv_id = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectChannel(
|
||||||
|
channel: epgGrabber.Channel,
|
||||||
|
searchIndex,
|
||||||
|
feedsGroupedByChannelId: Dictionary,
|
||||||
|
channelsKeyById: Dictionary
|
||||||
|
): Promise<string> {
|
||||||
|
const query = escapeRegex(channel.name)
|
||||||
|
const similarChannels = searchIndex
|
||||||
|
.search(query)
|
||||||
|
.map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
|
||||||
|
|
||||||
|
const selected: ChoiceValue = await select({
|
||||||
|
message: `Select channel ID for "${channel.name}" (${channel.site_id}):`,
|
||||||
|
choices: getChannelChoises(new Collection(similarChannels)),
|
||||||
|
pageSize: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
switch (selected.type) {
|
||||||
|
case 'skip':
|
||||||
|
return '-'
|
||||||
|
case 'type': {
|
||||||
|
const typedChannelId = await input({ message: ' Channel ID:' })
|
||||||
|
if (!typedChannelId) return ''
|
||||||
|
const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId)
|
||||||
|
if (selectedFeedId === '-') return typedChannelId
|
||||||
|
return [typedChannelId, selectedFeedId].join('@')
|
||||||
|
}
|
||||||
|
case 'channel': {
|
||||||
|
const selectedChannel = selected.value
|
||||||
|
if (!selectedChannel) return ''
|
||||||
|
const selectedFeedId = await selectFeed(selectedChannel.id || '', feedsGroupedByChannelId)
|
||||||
|
if (selectedFeedId === '-') return selectedChannel.id || ''
|
||||||
|
return [selectedChannel.id, selectedFeedId].join('@')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise<string> {
|
||||||
|
const channelFeeds = feedsGroupedByChannelId.has(channelId)
|
||||||
|
? new Collection(feedsGroupedByChannelId.get(channelId))
|
||||||
|
: new Collection()
|
||||||
|
const choices = getFeedChoises(channelFeeds)
|
||||||
|
|
||||||
|
const selected: ChoiceValue = await select({
|
||||||
|
message: `Select feed ID for "${channelId}":`,
|
||||||
|
choices,
|
||||||
|
pageSize: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
switch (selected.type) {
|
||||||
|
case 'skip':
|
||||||
|
return '-'
|
||||||
|
case 'type':
|
||||||
|
return await input({ message: ' Feed ID:', default: 'SD' })
|
||||||
|
case 'feed':
|
||||||
|
const selectedFeed = selected.value
|
||||||
|
if (!selectedFeed) return ''
|
||||||
|
return selectedFeed.id || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChannelChoises(channels: Collection): Choice[] {
|
||||||
|
const choises: Choice[] = []
|
||||||
|
|
||||||
|
channels.forEach((channel: Channel) => {
|
||||||
|
const names = new Collection([channel.name, ...channel.getAltNames().all()]).uniq().join(', ')
|
||||||
|
|
||||||
|
choises.push({
|
||||||
|
value: {
|
||||||
|
type: 'channel',
|
||||||
|
value: channel
|
||||||
|
},
|
||||||
|
name: `${channel.id} (${names})`,
|
||||||
|
short: `${channel.id}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
|
|
||||||
|
return choises
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFeedChoises(feeds: Collection): Choice[] {
|
||||||
|
const choises: Choice[] = []
|
||||||
|
|
||||||
|
feeds.forEach((feed: Feed) => {
|
||||||
|
let name = `${feed.id} (${feed.name})`
|
||||||
|
if (feed.isMain) name += ' [main]'
|
||||||
|
|
||||||
|
choises.push({
|
||||||
|
value: {
|
||||||
|
type: 'feed',
|
||||||
|
value: feed
|
||||||
|
},
|
||||||
|
default: feed.isMain,
|
||||||
|
name,
|
||||||
|
short: feed.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
choises.push({ name: 'Type...', value: { type: 'type' } })
|
||||||
|
choises.push({ name: 'Skip', value: { type: 'skip' } })
|
||||||
|
|
||||||
|
return choises
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(filepath: string, channelList: ChannelList) {
|
||||||
|
if (!storage.existsSync(filepath)) return
|
||||||
|
storage.saveSync(filepath, channelList.toString())
|
||||||
|
logger.info(`\nFile '${filepath}' successfully saved`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(string: string) {
|
||||||
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
|
}
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import { DATA_DIR } from '../../constants'
|
|
||||||
import { Storage, Collection, Dictionary, Logger } from '@freearhey/core'
|
|
||||||
import { ChannelsParser, XML, ApiChannel } from '../../core'
|
|
||||||
import { Channel } from 'epg-grabber'
|
|
||||||
import { transliterate } from 'transliteration'
|
|
||||||
import nodeCleanup from 'node-cleanup'
|
|
||||||
import { program } from 'commander'
|
|
||||||
import inquirer, { QuestionCollection } from 'inquirer'
|
|
||||||
import sj from '@freearhey/search-js'
|
|
||||||
|
|
||||||
program
|
|
||||||
.argument('<filepath>', 'Path to *.channels.xml file to edit')
|
|
||||||
.option('-c, --country <name>', 'Default country (ISO 3166 code)', 'US')
|
|
||||||
.parse(process.argv)
|
|
||||||
|
|
||||||
const filepath = program.args[0]
|
|
||||||
const programOptions = program.opts()
|
|
||||||
const defaultCountry = programOptions.country.toLowerCase()
|
|
||||||
const newLabel = ' [new]'
|
|
||||||
|
|
||||||
let options = new Collection()
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const storage = new Storage()
|
|
||||||
|
|
||||||
if (!(await storage.exists(filepath))) {
|
|
||||||
throw new Error(`File "${filepath}" does not exists`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = new ChannelsParser({ storage })
|
|
||||||
|
|
||||||
const parsedChannels = await parser.parse(filepath)
|
|
||||||
options = parsedChannels.map((channel: Channel): { channel: Channel; delete: boolean } => {
|
|
||||||
return {
|
|
||||||
channel,
|
|
||||||
delete: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
|
||||||
const channelsContent = await dataStorage.json('channels.json')
|
|
||||||
|
|
||||||
const channelsIndex = sj.createIndex(channelsContent)
|
|
||||||
|
|
||||||
const buffer = new Dictionary()
|
|
||||||
for (let option of options.all()) {
|
|
||||||
const channel: Channel = option.channel
|
|
||||||
if (channel.xmltv_id) {
|
|
||||||
if (channel.xmltv_id !== '-') {
|
|
||||||
buffer.set(`${channel.xmltv_id}/${channel.lang}`, true)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const choices = getOptions(channelsIndex, channel)
|
|
||||||
const question: QuestionCollection = {
|
|
||||||
name: 'option',
|
|
||||||
message: `Choose xmltv_id for "${channel.name}" (${channel.site_id}):`,
|
|
||||||
type: 'list',
|
|
||||||
choices,
|
|
||||||
pageSize: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
await inquirer.prompt(question).then(async selected => {
|
|
||||||
switch (selected.option) {
|
|
||||||
case 'Overwrite':
|
|
||||||
const input = await getInput(channel)
|
|
||||||
channel.xmltv_id = input.xmltv_id
|
|
||||||
break
|
|
||||||
case 'Skip':
|
|
||||||
channel.xmltv_id = '-'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
const [, xmltv_id] = selected.option
|
|
||||||
.replace(/ \[.*\]/, '')
|
|
||||||
.split('|')
|
|
||||||
.map((i: string) => i.trim().replace(newLabel, ''))
|
|
||||||
channel.xmltv_id = xmltv_id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = buffer.has(`${channel.xmltv_id}/${channel.lang}`)
|
|
||||||
if (found) {
|
|
||||||
const question: QuestionCollection = {
|
|
||||||
name: 'option',
|
|
||||||
message: `"${channel.xmltv_id}" already on the list. Choose an option:`,
|
|
||||||
type: 'list',
|
|
||||||
choices: ['Skip', 'Add', 'Delete'],
|
|
||||||
pageSize: 5
|
|
||||||
}
|
|
||||||
await inquirer.prompt(question).then(async selected => {
|
|
||||||
switch (selected.option) {
|
|
||||||
case 'Skip':
|
|
||||||
channel.xmltv_id = '-'
|
|
||||||
break
|
|
||||||
case 'Delete':
|
|
||||||
option.delete = true
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (channel.xmltv_id !== '-') {
|
|
||||||
buffer.set(`${channel.xmltv_id}/${channel.lang}`, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
const logger = new Logger()
|
|
||||||
const storage = new Storage()
|
|
||||||
|
|
||||||
if (!storage.existsSync(filepath)) return
|
|
||||||
|
|
||||||
const channels = options
|
|
||||||
.filter((option: { channel: Channel; delete: boolean }) => !option.delete)
|
|
||||||
.map((option: { channel: Channel; delete: boolean }) => option.channel)
|
|
||||||
|
|
||||||
const xml = new XML(channels)
|
|
||||||
|
|
||||||
storage.saveSync(filepath, xml.toString())
|
|
||||||
|
|
||||||
logger.info(`\nFile '${filepath}' successfully saved`)
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeCleanup(() => {
|
|
||||||
save()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function getInput(channel: Channel) {
|
|
||||||
const name = channel.name.trim()
|
|
||||||
const input = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
name: 'xmltv_id',
|
|
||||||
message: ' ID:',
|
|
||||||
type: 'input',
|
|
||||||
default: generateCode(name, defaultCountry)
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
return { name, xmltv_id: input['xmltv_id'] }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOptions(channelsIndex, channel: Channel) {
|
|
||||||
const channelId = generateCode(channel.name, defaultCountry)
|
|
||||||
const query = channel.name
|
|
||||||
.replace(/\s(SD|TV|HD|SD\/HD|HDTV)$/i, '')
|
|
||||||
.replace(/(\(|\)|,)/gi, '')
|
|
||||||
.replace(/\-/gi, ' ')
|
|
||||||
.replace(/\+/gi, '')
|
|
||||||
const similar = channelsIndex.search(query).map(item => new ApiChannel(item))
|
|
||||||
|
|
||||||
const variants = new Collection()
|
|
||||||
variants.add(`${channel.name.trim()} | ${channelId}${newLabel}`)
|
|
||||||
similar.forEach((_channel: ApiChannel) => {
|
|
||||||
const altNames = _channel.altNames.notEmpty() ? ` (${_channel.altNames.join(',')})` : ''
|
|
||||||
const closed = _channel.closed ? ` [closed:${_channel.closed}]` : ''
|
|
||||||
const replacedBy = _channel.replacedBy ? `[replaced_by:${_channel.replacedBy}]` : ''
|
|
||||||
|
|
||||||
variants.add(`${_channel.name}${altNames} | ${_channel.id}${closed}${replacedBy}`)
|
|
||||||
})
|
|
||||||
variants.add('Overwrite')
|
|
||||||
variants.add('Skip')
|
|
||||||
|
|
||||||
return variants.all()
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateCode(name: string, country: string) {
|
|
||||||
const channelId: string = transliterate(name)
|
|
||||||
.replace(/\+/gi, 'Plus')
|
|
||||||
.replace(/^&/gi, 'And')
|
|
||||||
.replace(/[^a-z\d]+/gi, '')
|
|
||||||
|
|
||||||
return `${channelId}.${country}`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import chalk from 'chalk'
|
||||||
|
import { program } from 'commander'
|
||||||
|
import { Storage, File } from '@freearhey/core'
|
||||||
|
import { XmlDocument, XsdValidator, XmlValidateError, ErrorDetail } from 'libxml2-wasm'
|
||||||
|
|
||||||
|
const xsd = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
|
||||||
|
<xs:element name="channels">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="unbounded" ref="channel"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="channel">
|
||||||
|
<xs:complexType mixed="true">
|
||||||
|
<xs:attribute use="required" ref="site"/>
|
||||||
|
<xs:attribute use="required" ref="lang"/>
|
||||||
|
<xs:attribute use="required" ref="site_id"/>
|
||||||
|
<xs:attribute name="xmltv_id" use="required" type="xs:string"/>
|
||||||
|
<xs:attribute name="logo" type="xs:string"/>
|
||||||
|
<xs:attribute name="lcn" type="xs:string"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:attribute name="site">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:minLength value="1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="site_id">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:minLength value="1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="lang">
|
||||||
|
<xs:simpleType>
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:minLength value="1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:schema>`
|
||||||
|
|
||||||
|
program.argument('[filepath...]', 'Path to *.channels.xml files to check').parse(process.argv)
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const storage = new Storage()
|
||||||
|
|
||||||
|
let errors: ErrorDetail[] = []
|
||||||
|
|
||||||
|
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
||||||
|
for (const filepath of files) {
|
||||||
|
const file = new File(filepath)
|
||||||
|
if (file.extension() !== 'xml') continue
|
||||||
|
|
||||||
|
const xml = await storage.load(filepath)
|
||||||
|
|
||||||
|
let localErrors: ErrorDetail[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schema = XmlDocument.fromString(xsd)
|
||||||
|
const validator = XsdValidator.fromDoc(schema)
|
||||||
|
const doc = XmlDocument.fromString(xml)
|
||||||
|
|
||||||
|
validator.validate(doc)
|
||||||
|
|
||||||
|
schema.dispose()
|
||||||
|
validator.dispose()
|
||||||
|
doc.dispose()
|
||||||
|
} catch (_error) {
|
||||||
|
const error = _error as XmlValidateError
|
||||||
|
|
||||||
|
localErrors = localErrors.concat(error.details)
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.split('\n').forEach((line: string, lineIndex: number) => {
|
||||||
|
const found = line.match(/='/)
|
||||||
|
if (found) {
|
||||||
|
const colIndex = found.index || 0
|
||||||
|
localErrors.push({
|
||||||
|
line: lineIndex + 1,
|
||||||
|
col: colIndex + 1,
|
||||||
|
message: 'Single quotes cannot be used in attributes'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (localErrors.length) {
|
||||||
|
console.log(`\n${chalk.underline(filepath)}`)
|
||||||
|
localErrors.forEach((error: ErrorDetail) => {
|
||||||
|
const position = `${error.line}:${error.col}`
|
||||||
|
console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
errors = errors.concat(localErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
console.log(chalk.red(`\n${errors.length} error(s)`))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import chalk from 'chalk'
|
|
||||||
import libxml, { ValidationError } from 'libxmljs2'
|
|
||||||
import { program } from 'commander'
|
|
||||||
import { Logger, Storage, File } from '@freearhey/core'
|
|
||||||
|
|
||||||
const xsd = `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
|
|
||||||
<xs:element name="channels">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="unbounded" ref="channel"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="channel">
|
|
||||||
<xs:complexType mixed="true">
|
|
||||||
<xs:attribute name="site" use="required" type="xs:string"/>
|
|
||||||
<xs:attribute name="lang" use="required" type="xs:string"/>
|
|
||||||
<xs:attribute name="site_id" use="required" type="xs:string"/>
|
|
||||||
<xs:attribute name="xmltv_id" use="required" type="xs:string"/>
|
|
||||||
<xs:attribute name="logo" type="xs:string"/>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:schema>`
|
|
||||||
|
|
||||||
program
|
|
||||||
.option(
|
|
||||||
'-c, --channels <path>',
|
|
||||||
'Path to channels.xml file to validate',
|
|
||||||
'sites/**/*.channels.xml'
|
|
||||||
)
|
|
||||||
.parse(process.argv)
|
|
||||||
|
|
||||||
const options = program.opts()
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const logger = new Logger()
|
|
||||||
const storage = new Storage()
|
|
||||||
|
|
||||||
logger.info('options:')
|
|
||||||
logger.tree(options)
|
|
||||||
|
|
||||||
let errors: ValidationError[] = []
|
|
||||||
|
|
||||||
const files: string[] = await storage.list(options.channels)
|
|
||||||
for (const filepath of files) {
|
|
||||||
const file = new File(filepath)
|
|
||||||
if (file.extension() !== 'xml') continue
|
|
||||||
|
|
||||||
const xml = await storage.load(filepath)
|
|
||||||
|
|
||||||
let localErrors: ValidationError[] = []
|
|
||||||
|
|
||||||
const xsdDoc = libxml.parseXml(xsd)
|
|
||||||
const doc = libxml.parseXml(xml)
|
|
||||||
|
|
||||||
if (!doc.validate(xsdDoc)) {
|
|
||||||
localErrors = doc.validationErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localErrors.length) {
|
|
||||||
console.log(`\n${chalk.underline(filepath)}`)
|
|
||||||
localErrors.forEach((error: ValidationError) => {
|
|
||||||
const position = `${error.line}:${error.column}`
|
|
||||||
console.log(` ${chalk.gray(position.padEnd(4, ' '))} ${error.message.trim()}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
errors = errors.concat(localErrors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
console.log(chalk.red(`\n${errors.length} error(s)`))
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,93 +1,86 @@
|
|||||||
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 path from 'path'
|
import epgGrabber from 'epg-grabber'
|
||||||
|
import { Command } from 'commander'
|
||||||
const program = new Command()
|
|
||||||
program
|
const program = new Command()
|
||||||
.requiredOption('-c, --config <config>', 'Config file')
|
program
|
||||||
.option('-s, --set [args...]', 'Set custom arguments')
|
.requiredOption('-c, --config <config>', 'Config file')
|
||||||
.option('-o, --output <output>', 'Output file')
|
.option('-s, --set [args...]', 'Set custom arguments')
|
||||||
.parse(process.argv)
|
.option('-o, --output <output>', 'Output file')
|
||||||
|
.parse(process.argv)
|
||||||
type ParseOptions = {
|
|
||||||
config: string
|
interface ParseOptions {
|
||||||
set?: string
|
config: string
|
||||||
output?: string
|
set?: string
|
||||||
clean?: boolean
|
output?: string
|
||||||
}
|
clean?: boolean
|
||||||
|
}
|
||||||
const options: ParseOptions = program.opts()
|
|
||||||
|
const options: ParseOptions = program.opts()
|
||||||
async function main() {
|
|
||||||
const storage = new Storage()
|
async function main() {
|
||||||
const parser = new ChannelsParser({ storage })
|
function isPromise(promise: object[] | Promise<object[]>) {
|
||||||
const logger = new Logger()
|
return (
|
||||||
const file = new File(options.config)
|
!!promise &&
|
||||||
const dir = file.dirname()
|
typeof promise === 'object' &&
|
||||||
const config = require(path.resolve(options.config))
|
typeof (promise as Promise<object[]>).then === 'function'
|
||||||
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
)
|
||||||
|
}
|
||||||
let channels = new Collection()
|
|
||||||
if (await storage.exists(outputFilepath)) {
|
const storage = new Storage()
|
||||||
channels = await parser.parse(outputFilepath)
|
const logger = new Logger()
|
||||||
}
|
const parser = new ChannelsParser({ storage })
|
||||||
|
const file = new File(options.config)
|
||||||
const args: {
|
const dir = file.dirname()
|
||||||
[key: string]: string
|
const config = (await import(pathToFileURL(options.config).toString())).default
|
||||||
} = {}
|
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
|
||||||
|
|
||||||
if (Array.isArray(options.set)) {
|
let channelList = new ChannelList({ channels: [] })
|
||||||
options.set.forEach((arg: string) => {
|
if (await storage.exists(outputFilepath)) {
|
||||||
const [key, value] = arg.split(':')
|
channelList = await parser.parse(outputFilepath)
|
||||||
args[key] = value
|
}
|
||||||
})
|
|
||||||
}
|
const args: Record<string, string> = {}
|
||||||
|
|
||||||
let parsedChannels = config.channels(args)
|
if (Array.isArray(options.set)) {
|
||||||
if (isPromise(parsedChannels)) {
|
options.set.forEach((arg: string) => {
|
||||||
parsedChannels = await parsedChannels
|
const [key, value] = arg.split(':')
|
||||||
}
|
args[key] = value
|
||||||
parsedChannels = parsedChannels.map((channel: Channel) => {
|
})
|
||||||
channel.site = config.site
|
}
|
||||||
|
|
||||||
return channel
|
let parsedChannels = config.channels(args)
|
||||||
})
|
if (isPromise(parsedChannels)) {
|
||||||
|
parsedChannels = await parsedChannels
|
||||||
let output = new Collection()
|
}
|
||||||
parsedChannels.forEach((channel: Channel) => {
|
parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
|
||||||
const found: Channel | undefined = channels.first(
|
channel.site = config.site
|
||||||
(_channel: Channel) => _channel.site_id == channel.site_id
|
|
||||||
)
|
return channel
|
||||||
|
})
|
||||||
if (found) {
|
|
||||||
channel.xmltv_id = found.xmltv_id
|
const newChannelList = new ChannelList({ channels: [] })
|
||||||
channel.lang = found.lang
|
parsedChannels.forEach((channel: epgGrabber.Channel) => {
|
||||||
}
|
if (!channel.site_id) return
|
||||||
|
|
||||||
output.add(channel)
|
const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
|
||||||
})
|
|
||||||
|
if (found) {
|
||||||
output = output.orderBy([
|
channel.xmltv_id = found.xmltv_id
|
||||||
(channel: Channel) => channel.lang || '_',
|
channel.lang = found.lang
|
||||||
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
|
}
|
||||||
(channel: Channel) => channel.site_id
|
|
||||||
])
|
newChannelList.add(channel)
|
||||||
|
})
|
||||||
const xml = new XML(output)
|
|
||||||
|
newChannelList.sort()
|
||||||
await storage.save(outputFilepath, xml.toString())
|
|
||||||
|
await storage.save(outputFilepath, newChannelList.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'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,102 +1,100 @@
|
|||||||
import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core'
|
import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
|
||||||
import { ChannelsParser, ApiChannel } from '../../core'
|
import { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import { program } from 'commander'
|
import { Storage, Dictionary, File } from '@freearhey/core'
|
||||||
import chalk from 'chalk'
|
import { DataLoaderData } from '../../types/dataLoader'
|
||||||
import langs from 'langs'
|
import { ChannelList } from '../../models'
|
||||||
import { DATA_DIR } from '../../constants'
|
import { DATA_DIR } from '../../constants'
|
||||||
import { Channel } from 'epg-grabber'
|
import epgGrabber from 'epg-grabber'
|
||||||
|
import { program } from 'commander'
|
||||||
program
|
import chalk from 'chalk'
|
||||||
.option(
|
import langs from 'langs'
|
||||||
'-c, --channels <path>',
|
|
||||||
'Path to channels.xml file to validate',
|
program.argument('[filepath...]', 'Path to *.channels.xml files to validate').parse(process.argv)
|
||||||
'sites/**/*.channels.xml'
|
|
||||||
)
|
interface ValidationError {
|
||||||
.parse(process.argv)
|
type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang'
|
||||||
|
name: string
|
||||||
const options = program.opts()
|
lang?: string
|
||||||
|
xmltv_id?: string
|
||||||
type ValidationError = {
|
site_id?: string
|
||||||
type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang'
|
logo?: string
|
||||||
name: string
|
}
|
||||||
lang?: string
|
|
||||||
xmltv_id?: string
|
async function main() {
|
||||||
site_id?: string
|
const processor = new DataProcessor()
|
||||||
logo?: string
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
}
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
|
const data: DataLoaderData = await loader.load()
|
||||||
async function main() {
|
const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
|
||||||
const logger = new Logger()
|
const parser = new ChannelsParser({
|
||||||
|
storage: new Storage()
|
||||||
logger.info('options:')
|
})
|
||||||
logger.tree(options)
|
|
||||||
|
let totalFiles = 0
|
||||||
const parser = new ChannelsParser({ storage: new Storage() })
|
let totalErrors = 0
|
||||||
|
let totalWarnings = 0
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
|
||||||
const channelsContent = await dataStorage.json('channels.json')
|
const storage = new Storage()
|
||||||
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
|
const files = program.args.length ? program.args : await storage.list('sites/**/*.channels.xml')
|
||||||
|
for (const filepath of files) {
|
||||||
let totalFiles = 0
|
const file = new File(filepath)
|
||||||
let totalErrors = 0
|
if (file.extension() !== 'xml') continue
|
||||||
const storage = new Storage()
|
|
||||||
const files: string[] = await storage.list(options.channels)
|
const channelList: ChannelList = await parser.parse(filepath)
|
||||||
for (const filepath of files) {
|
|
||||||
const file = new File(filepath)
|
const bufferBySiteId = new Dictionary()
|
||||||
if (file.extension() !== 'xml') continue
|
const errors: ValidationError[] = []
|
||||||
|
channelList.channels.forEach((channel: epgGrabber.Channel) => {
|
||||||
const parsedChannels = await parser.parse(filepath)
|
const bufferId: string = channel.site_id
|
||||||
|
if (bufferBySiteId.missing(bufferId)) {
|
||||||
const bufferById = new Dictionary()
|
bufferBySiteId.set(bufferId, true)
|
||||||
const bufferBySiteId = new Dictionary()
|
} else {
|
||||||
const errors: ValidationError[] = []
|
errors.push({ type: 'duplicate', ...channel })
|
||||||
parsedChannels.forEach((channel: Channel) => {
|
totalErrors++
|
||||||
const bufferSiteId: string = `${channel.site_id}:${channel.lang}`
|
}
|
||||||
if (bufferBySiteId.missing(bufferSiteId)) {
|
|
||||||
bufferBySiteId.set(bufferSiteId, true)
|
if (!langs.where('1', channel.lang ?? '')) {
|
||||||
} else {
|
errors.push({ type: 'wrong_lang', ...channel })
|
||||||
errors.push({ type: 'duplicate', ...channel })
|
totalErrors++
|
||||||
totalErrors++
|
}
|
||||||
}
|
|
||||||
|
if (!channel.xmltv_id) return
|
||||||
if (!langs.where('1', channel.lang)) {
|
const [channelId, feedId] = channel.xmltv_id.split('@')
|
||||||
errors.push({ type: 'wrong_lang', ...channel })
|
|
||||||
totalErrors++
|
const foundChannel = channelsKeyById.get(channelId)
|
||||||
}
|
if (!foundChannel) {
|
||||||
|
errors.push({ type: 'wrong_channel_id', ...channel })
|
||||||
if (!channel.xmltv_id) return
|
totalWarnings++
|
||||||
|
}
|
||||||
const foundChannel = channels.first(
|
|
||||||
(_channel: ApiChannel) => _channel.id === channel.xmltv_id
|
if (feedId) {
|
||||||
)
|
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||||
if (!foundChannel) {
|
if (!foundFeed) {
|
||||||
errors.push({ type: 'wrong_xmltv_id', ...channel })
|
errors.push({ type: 'wrong_feed_id', ...channel })
|
||||||
totalErrors++
|
totalWarnings++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// if (foundChannel && foundChannel.replacedBy) {
|
})
|
||||||
// errors.push({ type: 'replaced', ...channel })
|
|
||||||
// totalErrors++
|
if (errors.length) {
|
||||||
// }
|
console.log(chalk.underline(filepath))
|
||||||
|
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
|
||||||
// if (foundChannel && foundChannel.closed && !foundChannel.replacedBy) {
|
console.log()
|
||||||
// errors.push({ type: 'closed', ...channel })
|
totalFiles++
|
||||||
// totalErrors++
|
}
|
||||||
// }
|
}
|
||||||
})
|
|
||||||
|
const totalProblems = totalWarnings + totalErrors
|
||||||
if (errors.length) {
|
if (totalProblems > 0) {
|
||||||
console.log(chalk.underline(filepath))
|
console.log(
|
||||||
console.table(errors, ['type', 'lang', 'xmltv_id', 'site_id', 'name'])
|
chalk.red(
|
||||||
console.log()
|
`${totalProblems} problems (${totalErrors} errors, ${totalWarnings} warnings) in ${totalFiles} file(s)`
|
||||||
totalFiles++
|
)
|
||||||
}
|
)
|
||||||
}
|
if (totalErrors > 0) {
|
||||||
|
process.exit(1)
|
||||||
if (totalErrors > 0) {
|
}
|
||||||
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
|
}
|
||||||
process.exit(1)
|
}
|
||||||
}
|
|
||||||
}
|
main()
|
||||||
|
|
||||||
main()
|
|
||||||
|
|||||||
+133
-117
@@ -1,117 +1,133 @@
|
|||||||
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
|
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
|
||||||
import { program } from 'commander'
|
import { QueueCreator, Job, ChannelsParser } from '../../core'
|
||||||
import { CronJob } from 'cron'
|
import { Option, program } from 'commander'
|
||||||
import { QueueCreator, Job, ChannelsParser } from '../../core'
|
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
|
||||||
.option('-s, --site <name>', 'Name of the site to parse')
|
.addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
|
||||||
.option(
|
.addOption(
|
||||||
'-c, --channels <path>',
|
new Option(
|
||||||
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
|
'-c, --channels <path>',
|
||||||
)
|
'Path to *.channels.xml file (required if the "--site" attribute is not specified)'
|
||||||
.option('-o, --output <path>', 'Path to output file', 'guide.xml')
|
)
|
||||||
.option('-l, --lang <code>', 'Filter channels by language (ISO 639-2 code)')
|
)
|
||||||
.option('-t, --timeout <milliseconds>', 'Override the default timeout for each request')
|
.addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml'))
|
||||||
.option('-d, --delay <milliseconds>', 'Override the default delay between request')
|
.addOption(new Option('-l, --lang <codes>', 'Filter channels by languages (ISO 639-1 codes)'))
|
||||||
.option(
|
.addOption(
|
||||||
'--days <days>',
|
new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env(
|
||||||
'Override the number of days for which the program will be loaded (defaults to the value from the site config)',
|
'TIMEOUT'
|
||||||
value => parseInt(value)
|
)
|
||||||
)
|
)
|
||||||
.option(
|
.addOption(
|
||||||
'--maxConnections <number>',
|
new Option('-d, --delay <milliseconds>', 'Override the default delay between request').env(
|
||||||
'Limit on the number of concurrent requests',
|
'DELAY'
|
||||||
value => parseInt(value),
|
)
|
||||||
1
|
)
|
||||||
)
|
.addOption(new Option('-x, --proxy <url>', 'Use the specified proxy').env('PROXY'))
|
||||||
.option('--cron <expression>', 'Schedule a script run (example: "0 0 * * *")')
|
.addOption(
|
||||||
.option('--gzip', 'Create a compressed version of the guide as well', false)
|
new Option(
|
||||||
.parse(process.argv)
|
'--days <days>',
|
||||||
|
'Override the number of days for which the program will be loaded (defaults to the value from the site config)'
|
||||||
export type GrabOptions = {
|
)
|
||||||
site?: string
|
.argParser(value => parseInt(value))
|
||||||
channels?: string
|
.env('DAYS')
|
||||||
output: string
|
)
|
||||||
gzip: boolean
|
.addOption(
|
||||||
maxConnections: number
|
new Option('--maxConnections <number>', 'Limit on the number of concurrent requests')
|
||||||
timeout?: string
|
.default(1)
|
||||||
delay?: string
|
.env('MAX_CONNECTIONS')
|
||||||
lang?: string
|
)
|
||||||
days?: number
|
.addOption(
|
||||||
cron?: string
|
new Option('--gzip', 'Create a compressed version of the guide as well')
|
||||||
}
|
.default(false)
|
||||||
|
.env('GZIP')
|
||||||
const options: GrabOptions = program.opts()
|
)
|
||||||
|
.addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL'))
|
||||||
async function main() {
|
.parse()
|
||||||
if (!options.site && !options.channels)
|
|
||||||
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
|
export interface GrabOptions {
|
||||||
|
site?: string
|
||||||
const logger = new Logger()
|
channels?: string
|
||||||
|
output: string
|
||||||
logger.start('starting...')
|
gzip: boolean
|
||||||
|
curl: boolean
|
||||||
logger.info('config:')
|
maxConnections: number
|
||||||
logger.tree(options)
|
timeout?: string
|
||||||
|
delay?: string
|
||||||
logger.info('loading channels...')
|
lang?: string
|
||||||
const storage = new Storage()
|
days?: number
|
||||||
const parser = new ChannelsParser({ storage })
|
proxy?: string
|
||||||
|
}
|
||||||
let files: string[] = []
|
|
||||||
if (options.site) {
|
const options: GrabOptions = program.opts()
|
||||||
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
|
|
||||||
pattern = pattern.replace(/\\/g, '/')
|
async function main() {
|
||||||
files = await storage.list(pattern)
|
if (!options.site && !options.channels)
|
||||||
} else if (options.channels) {
|
throw new Error('One of the arguments must be presented: `--site` or `--channels`')
|
||||||
files = await storage.list(options.channels)
|
|
||||||
}
|
const logger = new Logger()
|
||||||
|
|
||||||
let parsedChannels = new Collection()
|
logger.start('starting...')
|
||||||
for (const filepath of files) {
|
|
||||||
parsedChannels = parsedChannels.concat(await parser.parse(filepath))
|
logger.info('config:')
|
||||||
}
|
logger.tree(options)
|
||||||
if (options.lang) {
|
|
||||||
parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang)
|
logger.info('loading channels...')
|
||||||
}
|
const storage = new Storage()
|
||||||
logger.info(` found ${parsedChannels.count()} channel(s)`)
|
const parser = new ChannelsParser({ storage })
|
||||||
|
|
||||||
let runIndex = 1
|
let files: string[] = []
|
||||||
if (options.cron) {
|
if (options.site) {
|
||||||
const cronJob = new CronJob(options.cron, async () => {
|
let pattern = path.join(SITES_DIR, options.site, '*.channels.xml')
|
||||||
logger.info(`run #${runIndex}:`)
|
pattern = pattern.replace(/\\/g, '/')
|
||||||
await runJob({ logger, parsedChannels })
|
files = await storage.list(pattern)
|
||||||
runIndex++
|
} else if (options.channels) {
|
||||||
})
|
files = await storage.list(options.channels)
|
||||||
cronJob.start()
|
}
|
||||||
} else {
|
|
||||||
logger.info(`run #${runIndex}:`)
|
let channels = new Collection()
|
||||||
runJob({ logger, parsedChannels })
|
for (const filepath of files) {
|
||||||
}
|
const channelList: ChannelList = await parser.parse(filepath)
|
||||||
}
|
|
||||||
|
channels = channels.concat(channelList.channels)
|
||||||
main()
|
}
|
||||||
|
|
||||||
async function runJob({ logger, parsedChannels }: { logger: Logger; parsedChannels: Collection }) {
|
if (options.lang) {
|
||||||
const timer = new Timer()
|
channels = channels.filter((channel: Channel) => {
|
||||||
timer.start()
|
if (!options.lang || !channel.lang) return true
|
||||||
|
|
||||||
const queueCreator = new QueueCreator({
|
return options.lang.includes(channel.lang)
|
||||||
parsedChannels,
|
})
|
||||||
logger,
|
}
|
||||||
options
|
|
||||||
})
|
logger.info(` found ${channels.count()} channel(s)`)
|
||||||
const queue = await queueCreator.create()
|
|
||||||
const job = new Job({
|
logger.info('run:')
|
||||||
queue,
|
runJob({ logger, channels })
|
||||||
logger,
|
}
|
||||||
options
|
|
||||||
})
|
main()
|
||||||
|
|
||||||
await job.run()
|
async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
|
||||||
|
const timer = new Timer()
|
||||||
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
timer.start()
|
||||||
}
|
|
||||||
|
const queueCreator = new QueueCreator({
|
||||||
|
channels,
|
||||||
|
logger,
|
||||||
|
options
|
||||||
|
})
|
||||||
|
const queue = await queueCreator.create()
|
||||||
|
const job = new Job({
|
||||||
|
queue,
|
||||||
|
logger,
|
||||||
|
options
|
||||||
|
})
|
||||||
|
|
||||||
|
await job.run()
|
||||||
|
|
||||||
|
logger.success(` done in ${timer.format('HH[h] mm[m] ss[s]')}`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Logger, Storage } from '@freearhey/core'
|
||||||
|
import { SITES_DIR } from '../../constants'
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { program } from 'commander'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
|
||||||
|
program.argument('<site>', 'Domain name of the site').parse(process.argv)
|
||||||
|
|
||||||
|
const domain = program.args[0]
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const storage = new Storage(SITES_DIR)
|
||||||
|
const logger = new Logger()
|
||||||
|
|
||||||
|
logger.info(`Initializing "${domain}"...\r\n`)
|
||||||
|
|
||||||
|
const dir = domain
|
||||||
|
if (await storage.exists(dir)) {
|
||||||
|
throw new Error(`Folder "${dir}" already exists`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await storage.createDir(dir)
|
||||||
|
|
||||||
|
logger.info(`Creating "${dir}/${domain}.test.js"...`)
|
||||||
|
const testTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_test.js'), {
|
||||||
|
encoding: 'utf8'
|
||||||
|
})
|
||||||
|
await storage.save(`${dir}/${domain}.test.js`, testTemplate.replace(/<DOMAIN>/g, domain))
|
||||||
|
|
||||||
|
logger.info(`Creating "${dir}/${domain}.config.js"...`)
|
||||||
|
const configTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_config.js'), {
|
||||||
|
encoding: 'utf8'
|
||||||
|
})
|
||||||
|
await storage.save(`${dir}/${domain}.config.js`, configTemplate.replace(/<DOMAIN>/g, domain))
|
||||||
|
|
||||||
|
logger.info(`Creating "${dir}/readme.md"...`)
|
||||||
|
const readmeTemplate = fs.readFileSync(pathToFileURL('scripts/templates/_readme.md'), {
|
||||||
|
encoding: 'utf8'
|
||||||
|
})
|
||||||
|
await storage.save(`${dir}/readme.md`, readmeTemplate.replace(/<DOMAIN>/g, domain))
|
||||||
|
|
||||||
|
logger.info('\r\nDone')
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { IssueLoader, HTMLTable, ChannelsParser } from '../../core'
|
||||||
|
import { Logger, Storage, Collection } from '@freearhey/core'
|
||||||
|
import { ChannelList, Issue, Site } from '../../models'
|
||||||
|
import { SITES_DIR, ROOT_DIR } from '../../constants'
|
||||||
|
import { Channel } from 'epg-grabber'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const logger = new Logger({ level: -999 })
|
||||||
|
const issueLoader = new IssueLoader()
|
||||||
|
const sitesStorage = new Storage(SITES_DIR)
|
||||||
|
const sites = new Collection()
|
||||||
|
|
||||||
|
logger.info('loading channels...')
|
||||||
|
const channelsParser = new ChannelsParser({
|
||||||
|
storage: sitesStorage
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info('loading list of sites')
|
||||||
|
const folders = await sitesStorage.list('*/')
|
||||||
|
|
||||||
|
logger.info('loading issues...')
|
||||||
|
const issues = await issueLoader.load()
|
||||||
|
|
||||||
|
logger.info('putting the data together...')
|
||||||
|
const brokenGuideReports = issues.filter(issue =>
|
||||||
|
issue.labels.find((label: string) => label === 'broken guide')
|
||||||
|
)
|
||||||
|
for (const domain of folders) {
|
||||||
|
const filteredIssues = brokenGuideReports.filter(
|
||||||
|
(issue: Issue) => domain === issue.data.get('site')
|
||||||
|
)
|
||||||
|
|
||||||
|
const site = new Site({
|
||||||
|
domain,
|
||||||
|
issues: filteredIssues
|
||||||
|
})
|
||||||
|
|
||||||
|
const files = await sitesStorage.list(`${domain}/*.channels.xml`)
|
||||||
|
for (const filepath of files) {
|
||||||
|
const channelList: ChannelList = await channelsParser.parse(filepath)
|
||||||
|
|
||||||
|
site.totalChannels += channelList.channels.count()
|
||||||
|
site.markedChannels += channelList.channels
|
||||||
|
.filter((channel: Channel) => channel.xmltv_id)
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
sites.add(site)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('creating sites table...')
|
||||||
|
const tableData = new Collection()
|
||||||
|
sites.forEach((site: Site) => {
|
||||||
|
tableData.add([
|
||||||
|
{ value: `<a href="sites/${site.domain}">${site.domain}</a>` },
|
||||||
|
{ value: site.totalChannels, align: 'right' },
|
||||||
|
{ value: site.markedChannels, align: 'right' },
|
||||||
|
{ value: site.getStatus().emoji, align: 'center' },
|
||||||
|
{ value: site.getIssues().all().join(', ') }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info('updating sites.md...')
|
||||||
|
const table = new HTMLTable(tableData.all(), [
|
||||||
|
{ name: 'Site', align: 'left' },
|
||||||
|
{ name: 'Channels<br>(total / with xmltv-id)', colspan: 2, align: 'left' },
|
||||||
|
{ name: 'Status', align: 'left' },
|
||||||
|
{ name: 'Notes', align: 'left' }
|
||||||
|
])
|
||||||
|
const rootStorage = new Storage(ROOT_DIR)
|
||||||
|
const sitesTemplate = await new Storage().load('scripts/templates/_sites.md')
|
||||||
|
const sitesContent = sitesTemplate.replace('_TABLE_', table.toString())
|
||||||
|
await rootStorage.save('SITES.md', sitesContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
export const SITES_DIR = process.env.SITES_DIR || './sites'
|
export const ROOT_DIR = process.env.ROOT_DIR || '.'
|
||||||
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
|
export const SITES_DIR = process.env.SITES_DIR || './sites'
|
||||||
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
|
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
|
||||||
export const API_DIR = process.env.API_DIR || '.api'
|
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
|
||||||
|
export const API_DIR = process.env.API_DIR || '.api'
|
||||||
|
export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites'
|
||||||
|
export const TESTING = process.env.NODE_ENV === 'test' ? true : false
|
||||||
|
export const OWNER = 'iptv-org'
|
||||||
|
export const REPO = 'epg'
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { Collection } from '@freearhey/core'
|
|
||||||
|
|
||||||
type ApiChannelProps = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
alt_names: string[]
|
|
||||||
network: string
|
|
||||||
owners: string[]
|
|
||||||
country: string
|
|
||||||
subdivision: string
|
|
||||||
city: string
|
|
||||||
broadcast_area: string[]
|
|
||||||
languages: string[]
|
|
||||||
categories: string[]
|
|
||||||
is_nsfw: boolean
|
|
||||||
launched: string
|
|
||||||
closed: string
|
|
||||||
replaced_by: string
|
|
||||||
website: string
|
|
||||||
logo: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ApiChannel {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
altNames: Collection
|
|
||||||
network: string
|
|
||||||
owners: Collection
|
|
||||||
country: string
|
|
||||||
subdivision: string
|
|
||||||
city: string
|
|
||||||
broadcastArea: Collection
|
|
||||||
languages: Collection
|
|
||||||
categories: Collection
|
|
||||||
isNSFW: boolean
|
|
||||||
launched: string
|
|
||||||
closed: string
|
|
||||||
replacedBy: string
|
|
||||||
website: string
|
|
||||||
logo: string
|
|
||||||
|
|
||||||
constructor({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
alt_names,
|
|
||||||
network,
|
|
||||||
owners,
|
|
||||||
country,
|
|
||||||
subdivision,
|
|
||||||
city,
|
|
||||||
broadcast_area,
|
|
||||||
languages,
|
|
||||||
categories,
|
|
||||||
is_nsfw,
|
|
||||||
launched,
|
|
||||||
closed,
|
|
||||||
replaced_by,
|
|
||||||
website,
|
|
||||||
logo
|
|
||||||
}: ApiChannelProps) {
|
|
||||||
this.id = id
|
|
||||||
this.name = name
|
|
||||||
this.altNames = new Collection(alt_names)
|
|
||||||
this.network = network
|
|
||||||
this.owners = new Collection(owners)
|
|
||||||
this.country = country
|
|
||||||
this.subdivision = subdivision
|
|
||||||
this.city = city
|
|
||||||
this.broadcastArea = new Collection(broadcast_area)
|
|
||||||
this.languages = new Collection(languages)
|
|
||||||
this.categories = new Collection(categories)
|
|
||||||
this.isNSFW = is_nsfw
|
|
||||||
this.launched = launched
|
|
||||||
this.closed = closed
|
|
||||||
this.replacedBy = replaced_by
|
|
||||||
this.website = website
|
|
||||||
this.logo = logo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-59
@@ -1,59 +1,16 @@
|
|||||||
import { Logger, Storage } from '@freearhey/core'
|
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
|
||||||
import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios'
|
|
||||||
import cliProgress, { MultiBar } from 'cli-progress'
|
export class ApiClient {
|
||||||
import numeral from 'numeral'
|
instance: AxiosInstance
|
||||||
|
|
||||||
export class ApiClient {
|
constructor() {
|
||||||
progressBar: MultiBar
|
this.instance = axios.create({
|
||||||
client: AxiosInstance
|
baseURL: 'https://iptv-org.github.io/api',
|
||||||
storage: Storage
|
responseType: 'stream'
|
||||||
logger: Logger
|
})
|
||||||
|
}
|
||||||
constructor({ logger }: { logger: Logger }) {
|
|
||||||
this.logger = logger
|
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
|
||||||
this.client = axios.create({
|
return this.instance.get(url, options)
|
||||||
responseType: 'stream'
|
}
|
||||||
})
|
}
|
||||||
this.storage = new Storage()
|
|
||||||
this.progressBar = new cliProgress.MultiBar({
|
|
||||||
stopOnComplete: true,
|
|
||||||
hideCursor: true,
|
|
||||||
forceRedraw: true,
|
|
||||||
barsize: 36,
|
|
||||||
format(options, params, payload) {
|
|
||||||
const filename = payload.filename.padEnd(18, ' ')
|
|
||||||
const barsize = options.barsize || 40
|
|
||||||
const percent = (params.progress * 100).toFixed(2)
|
|
||||||
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
|
|
||||||
const total = numeral(params.total).format('0.0 b')
|
|
||||||
const completeSize = Math.round(params.progress * barsize)
|
|
||||||
const incompleteSize = barsize - completeSize
|
|
||||||
const bar =
|
|
||||||
options.barCompleteString && options.barIncompleteString
|
|
||||||
? options.barCompleteString.substr(0, completeSize) +
|
|
||||||
options.barGlue +
|
|
||||||
options.barIncompleteString.substr(0, incompleteSize)
|
|
||||||
: '-'.repeat(barsize)
|
|
||||||
|
|
||||||
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async download(filename: string) {
|
|
||||||
const stream = await this.storage.createStream(`/temp/data/${filename}`)
|
|
||||||
|
|
||||||
const bar = this.progressBar.create(0, 0, { filename })
|
|
||||||
|
|
||||||
this.client
|
|
||||||
.get(`https://iptv-org.github.io/api/${filename}`, {
|
|
||||||
onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) {
|
|
||||||
if (total) bar.setTotal(total)
|
|
||||||
bar.update(loaded, { speed: rate })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((response: AxiosResponse) => {
|
|
||||||
response.data.pipe(stream)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
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 = {
|
|
||||||
storage: Storage
|
interface ChannelsParserProps {
|
||||||
}
|
storage: Storage
|
||||||
|
}
|
||||||
export class ChannelsParser {
|
|
||||||
storage: Storage
|
export class ChannelsParser {
|
||||||
|
storage: Storage
|
||||||
constructor({ storage }: ChannelsParserProps) {
|
|
||||||
this.storage = storage
|
constructor({ storage }: ChannelsParserProps) {
|
||||||
}
|
this.storage = storage
|
||||||
|
}
|
||||||
async parse(filepath: string) {
|
|
||||||
let parsedChannels = new Collection()
|
async parse(filepath: string): Promise<ChannelList> {
|
||||||
|
const content = await this.storage.load(filepath)
|
||||||
const content = await this.storage.load(filepath)
|
const parsed = parseChannels(content)
|
||||||
const channels = parseChannels(content)
|
|
||||||
parsedChannels = parsedChannels.concat(new Collection(channels))
|
return new ChannelList({ channels: parsed })
|
||||||
|
}
|
||||||
return parsedChannels
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,33 +1,32 @@
|
|||||||
import { SiteConfig } from 'epg-grabber'
|
import { SiteConfig } from 'epg-grabber'
|
||||||
import _ from 'lodash'
|
import { pathToFileURL } from 'url'
|
||||||
import { pathToFileURL } from 'url'
|
|
||||||
|
export class ConfigLoader {
|
||||||
export class ConfigLoader {
|
async load(filepath: string): Promise<SiteConfig> {
|
||||||
async load(filepath: string): Promise<SiteConfig> {
|
const fileUrl = pathToFileURL(filepath).toString()
|
||||||
const fileUrl = pathToFileURL(filepath).toString()
|
const config = (await import(fileUrl)).default
|
||||||
const config = (await import(fileUrl)).default
|
const defaultConfig = {
|
||||||
const defaultConfig = {
|
days: 1,
|
||||||
days: 2,
|
delay: 0,
|
||||||
delay: 0,
|
output: 'guide.xml',
|
||||||
output: 'guide.xml',
|
request: {
|
||||||
request: {
|
method: 'GET',
|
||||||
method: 'GET',
|
maxContentLength: 5242880,
|
||||||
maxContentLength: 5242880,
|
timeout: 30000,
|
||||||
timeout: 30000,
|
withCredentials: true,
|
||||||
withCredentials: true,
|
jar: null,
|
||||||
jar: null,
|
responseType: 'arraybuffer',
|
||||||
responseType: 'arraybuffer',
|
cache: false,
|
||||||
cache: false,
|
headers: null,
|
||||||
headers: null,
|
data: null
|
||||||
data: null
|
},
|
||||||
},
|
maxConnections: 1,
|
||||||
maxConnections: 1,
|
site: undefined,
|
||||||
site: undefined,
|
url: undefined,
|
||||||
url: undefined,
|
parser: undefined,
|
||||||
parser: undefined,
|
channels: undefined
|
||||||
channels: undefined
|
}
|
||||||
}
|
|
||||||
|
return { ...defaultConfig, ...config } as SiteConfig
|
||||||
return _.merge(defaultConfig, config)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader'
|
||||||
|
import cliProgress, { MultiBar } from 'cli-progress'
|
||||||
|
import { Storage } from '@freearhey/core'
|
||||||
|
import { ApiClient } from './apiClient'
|
||||||
|
import numeral from 'numeral'
|
||||||
|
|
||||||
|
export class DataLoader {
|
||||||
|
client: ApiClient
|
||||||
|
storage: Storage
|
||||||
|
progressBar: MultiBar
|
||||||
|
|
||||||
|
constructor(props: DataLoaderProps) {
|
||||||
|
this.client = new ApiClient()
|
||||||
|
this.storage = props.storage
|
||||||
|
this.progressBar = new cliProgress.MultiBar({
|
||||||
|
stopOnComplete: true,
|
||||||
|
hideCursor: true,
|
||||||
|
forceRedraw: true,
|
||||||
|
barsize: 36,
|
||||||
|
format(options, params, payload) {
|
||||||
|
const filename = payload.filename.padEnd(18, ' ')
|
||||||
|
const barsize = options.barsize || 40
|
||||||
|
const percent = (params.progress * 100).toFixed(2)
|
||||||
|
const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A'
|
||||||
|
const total = numeral(params.total).format('0.0 b')
|
||||||
|
const completeSize = Math.round(params.progress * barsize)
|
||||||
|
const incompleteSize = barsize - completeSize
|
||||||
|
const bar =
|
||||||
|
options.barCompleteString && options.barIncompleteString
|
||||||
|
? options.barCompleteString.substr(0, completeSize) +
|
||||||
|
options.barGlue +
|
||||||
|
options.barIncompleteString.substr(0, incompleteSize)
|
||||||
|
: '-'.repeat(barsize)
|
||||||
|
|
||||||
|
return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<DataLoaderData> {
|
||||||
|
const [
|
||||||
|
countries,
|
||||||
|
regions,
|
||||||
|
subdivisions,
|
||||||
|
languages,
|
||||||
|
categories,
|
||||||
|
blocklist,
|
||||||
|
channels,
|
||||||
|
feeds,
|
||||||
|
timezones,
|
||||||
|
guides,
|
||||||
|
streams,
|
||||||
|
logos
|
||||||
|
] = await Promise.all([
|
||||||
|
this.storage.json('countries.json'),
|
||||||
|
this.storage.json('regions.json'),
|
||||||
|
this.storage.json('subdivisions.json'),
|
||||||
|
this.storage.json('languages.json'),
|
||||||
|
this.storage.json('categories.json'),
|
||||||
|
this.storage.json('blocklist.json'),
|
||||||
|
this.storage.json('channels.json'),
|
||||||
|
this.storage.json('feeds.json'),
|
||||||
|
this.storage.json('timezones.json'),
|
||||||
|
this.storage.json('guides.json'),
|
||||||
|
this.storage.json('streams.json'),
|
||||||
|
this.storage.json('logos.json')
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
countries,
|
||||||
|
regions,
|
||||||
|
subdivisions,
|
||||||
|
languages,
|
||||||
|
categories,
|
||||||
|
blocklist,
|
||||||
|
channels,
|
||||||
|
feeds,
|
||||||
|
timezones,
|
||||||
|
guides,
|
||||||
|
streams,
|
||||||
|
logos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(filename: string) {
|
||||||
|
if (!this.storage || !this.progressBar) return
|
||||||
|
|
||||||
|
const stream = await this.storage.createStream(filename)
|
||||||
|
const progressBar = this.progressBar.create(0, 0, { filename })
|
||||||
|
|
||||||
|
this.client
|
||||||
|
.get(filename, {
|
||||||
|
responseType: 'stream',
|
||||||
|
onDownloadProgress({ total, loaded, rate }) {
|
||||||
|
if (total) progressBar.setTotal(total)
|
||||||
|
progressBar.update(loaded, { speed: rate })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
response.data.pipe(stream)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Channel, Feed, GuideChannel, Logo, Stream } from '../models'
|
||||||
|
import { DataLoaderData } from '../types/dataLoader'
|
||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
|
||||||
|
export class DataProcessor {
|
||||||
|
|
||||||
|
process(data: DataLoaderData) {
|
||||||
|
let channels = new Collection(data.channels).map(data => new Channel(data))
|
||||||
|
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||||
|
|
||||||
|
const guideChannels = new Collection(data.guides).map(data => new GuideChannel(data))
|
||||||
|
const guideChannelsGroupedByStreamId = guideChannels.groupBy((channel: GuideChannel) =>
|
||||||
|
channel.getStreamId()
|
||||||
|
)
|
||||||
|
|
||||||
|
const streams = new Collection(data.streams).map(data => new Stream(data))
|
||||||
|
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||||
|
|
||||||
|
let feeds = new Collection(data.feeds).map(data =>
|
||||||
|
new Feed(data)
|
||||||
|
.withGuideChannels(guideChannelsGroupedByStreamId)
|
||||||
|
.withStreams(streamsGroupedById)
|
||||||
|
.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)
|
||||||
|
|
||||||
|
channels = channels.map((channel: Channel) =>
|
||||||
|
channel.withFeeds(feedsGroupedByChannelId).withLogos(logosGroupedByChannelId)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
guideChannelsGroupedByStreamId,
|
||||||
|
feedsGroupedByChannelId,
|
||||||
|
logosGroupedByChannelId,
|
||||||
|
logosGroupedByStreamId,
|
||||||
|
streamsGroupedById,
|
||||||
|
feedsKeyByStreamId,
|
||||||
|
channelsKeyById,
|
||||||
|
guideChannels,
|
||||||
|
channels,
|
||||||
|
streams,
|
||||||
|
feeds,
|
||||||
|
logos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-13
@@ -1,13 +1,14 @@
|
|||||||
const dayjs = require('dayjs')
|
import dayjs from 'dayjs'
|
||||||
const utc = require('dayjs/plugin/utc')
|
import utc from 'dayjs/plugin/utc'
|
||||||
dayjs.extend(utc)
|
|
||||||
|
dayjs.extend(utc)
|
||||||
const date = {}
|
|
||||||
|
const date = {}
|
||||||
date.getUTC = function (d = null) {
|
|
||||||
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
|
date.getUTC = function (d = null) {
|
||||||
|
if (typeof d === 'string') return dayjs.utc(d).startOf('d')
|
||||||
return dayjs.utc().startOf('d')
|
|
||||||
}
|
return dayjs.utc().startOf('d')
|
||||||
|
}
|
||||||
module.exports = date
|
|
||||||
|
export default date
|
||||||
|
|||||||
+105
-80
@@ -1,80 +1,105 @@
|
|||||||
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
|
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
|
||||||
import { Logger, Collection } from '@freearhey/core'
|
import { Logger, Collection } from '@freearhey/core'
|
||||||
import { Queue } from './'
|
import { Queue, ProxyParser } from './'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
import { TaskQueue, PromisyClass } from 'cwait'
|
import { TaskQueue, PromisyClass } from 'cwait'
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||||
type GrabberProps = {
|
|
||||||
logger: Logger
|
interface GrabberProps {
|
||||||
queue: Queue
|
logger: Logger
|
||||||
options: GrabOptions
|
queue: Queue
|
||||||
}
|
options: GrabOptions
|
||||||
|
}
|
||||||
export class Grabber {
|
|
||||||
logger: Logger
|
export class Grabber {
|
||||||
queue: Queue
|
logger: Logger
|
||||||
options: GrabOptions
|
queue: Queue
|
||||||
|
options: GrabOptions
|
||||||
constructor({ logger, queue, options }: GrabberProps) {
|
grabber: EPGGrabber | EPGGrabberMock
|
||||||
this.logger = logger
|
|
||||||
this.queue = queue
|
constructor({ logger, queue, options }: GrabberProps) {
|
||||||
this.options = options
|
this.logger = logger
|
||||||
this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber()
|
this.queue = queue
|
||||||
}
|
this.options = options
|
||||||
|
this.grabber = process.env.NODE_ENV === 'test' ? new EPGGrabberMock() : new EPGGrabber()
|
||||||
async grab(): Promise<{ channels: Collection; programs: Collection }> {
|
}
|
||||||
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
|
|
||||||
|
async grab(): Promise<{ channels: Collection; programs: Collection }> {
|
||||||
const total = this.queue.size()
|
const proxyParser = new ProxyParser()
|
||||||
|
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
|
||||||
const channels = new Collection()
|
|
||||||
let programs = new Collection()
|
const total = this.queue.size()
|
||||||
let i = 1
|
|
||||||
|
const channels = new Collection()
|
||||||
await Promise.all(
|
let programs = new Collection()
|
||||||
this.queue.items().map(
|
let i = 1
|
||||||
taskQueue.wrap(
|
|
||||||
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
|
await Promise.all(
|
||||||
const { channel, config, date } = queueItem
|
this.queue.items().map(
|
||||||
|
taskQueue.wrap(
|
||||||
channels.add(channel)
|
async (queueItem: { channel: Channel; config: SiteConfig; date: string }) => {
|
||||||
|
const { channel, config, date } = queueItem
|
||||||
if (this.options.timeout !== undefined) {
|
|
||||||
const timeout = parseInt(this.options.timeout)
|
channels.add(channel)
|
||||||
config.request = { ...config.request, ...{ timeout } }
|
|
||||||
}
|
if (this.options.timeout !== undefined) {
|
||||||
|
const timeout = parseInt(this.options.timeout)
|
||||||
if (this.options.delay !== undefined) {
|
config.request = { ...config.request, ...{ timeout } }
|
||||||
const delay = parseInt(this.options.delay)
|
}
|
||||||
config.delay = delay
|
|
||||||
}
|
if (this.options.delay !== undefined) {
|
||||||
|
const delay = parseInt(this.options.delay)
|
||||||
const _programs = await this.grabber.grab(
|
config.delay = delay
|
||||||
channel,
|
}
|
||||||
date,
|
|
||||||
config,
|
if (this.options.proxy !== undefined) {
|
||||||
(data: GrabCallbackData, error: Error | null) => {
|
const proxy = proxyParser.parse(this.options.proxy)
|
||||||
const { programs, date } = data
|
|
||||||
|
if (
|
||||||
this.logger.info(
|
proxy.protocol &&
|
||||||
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
|
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
|
||||||
channel.xmltv_id
|
) {
|
||||||
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
|
const socksProxyAgent = new SocksProxyAgent(this.options.proxy)
|
||||||
)
|
|
||||||
if (i < total) i++
|
config.request = {
|
||||||
|
...config.request,
|
||||||
if (error) {
|
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
|
||||||
this.logger.info(` ERR: ${error.message}`)
|
}
|
||||||
}
|
} else {
|
||||||
}
|
config.request = { ...config.request, ...{ proxy } }
|
||||||
)
|
}
|
||||||
|
}
|
||||||
programs = programs.concat(new Collection(_programs))
|
|
||||||
}
|
if (this.options.curl === true) {
|
||||||
)
|
config.curl = true
|
||||||
)
|
}
|
||||||
)
|
|
||||||
|
const _programs = await this.grabber.grab(
|
||||||
return { channels, programs }
|
channel,
|
||||||
}
|
date,
|
||||||
}
|
config,
|
||||||
|
(data: GrabCallbackData, error: Error | null) => {
|
||||||
|
const { programs, date } = data
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
` [${i}/${total}] ${channel.site} (${channel.lang}) - ${
|
||||||
|
channel.xmltv_id
|
||||||
|
} - ${date.format('MMM D, YYYY')} (${programs.length} programs)`
|
||||||
|
)
|
||||||
|
if (i < total) i++
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.logger.info(` ERR: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
programs = programs.concat(new Collection(_programs))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return { channels, programs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(), {
|
|
||||||
zone: '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 = await 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+111
-61
@@ -1,61 +1,111 @@
|
|||||||
import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core'
|
import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
|
||||||
import { OptionValues } from 'commander'
|
import epgGrabber from 'epg-grabber'
|
||||||
import { Channel, Program } from 'epg-grabber'
|
import { OptionValues } from 'commander'
|
||||||
import { Guide } from '.'
|
import { Channel, Feed, Guide } from '../models'
|
||||||
|
import path from 'path'
|
||||||
type GuideManagerProps = {
|
import { DataLoader, DataProcessor } from '.'
|
||||||
options: OptionValues
|
import { DataLoaderData } from '../types/dataLoader'
|
||||||
logger: Logger
|
import { DataProcessorData } from '../types/dataProcessor'
|
||||||
channels: Collection
|
import { DATA_DIR } from '../constants'
|
||||||
programs: Collection
|
|
||||||
}
|
interface GuideManagerProps {
|
||||||
|
options: OptionValues
|
||||||
export class GuideManager {
|
logger: Logger
|
||||||
options: OptionValues
|
channels: Collection
|
||||||
storage: Storage
|
programs: Collection
|
||||||
logger: Logger
|
}
|
||||||
channels: Collection
|
|
||||||
programs: Collection
|
export class GuideManager {
|
||||||
|
options: OptionValues
|
||||||
constructor({ channels, programs, logger, options }: GuideManagerProps) {
|
logger: Logger
|
||||||
this.options = options
|
channels: Collection
|
||||||
this.logger = logger
|
programs: Collection
|
||||||
this.channels = channels
|
|
||||||
this.programs = programs
|
constructor({ channels, programs, logger, options }: GuideManagerProps) {
|
||||||
this.storage = new Storage()
|
this.options = options
|
||||||
}
|
this.logger = logger
|
||||||
|
this.channels = channels
|
||||||
async createGuides() {
|
this.programs = programs
|
||||||
const pathTemplate = new StringTemplate(this.options.output)
|
}
|
||||||
|
|
||||||
const groupedChannels = this.channels
|
async createGuides() {
|
||||||
.orderBy([(channel: Channel) => channel.xmltv_id])
|
const pathTemplate = new StringTemplate(this.options.output)
|
||||||
.uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`)
|
|
||||||
.groupBy((channel: Channel) => {
|
const processor = new DataProcessor()
|
||||||
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
})
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
|
const data: DataLoaderData = await loader.load()
|
||||||
const groupedPrograms = this.programs
|
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
|
||||||
.orderBy([(program: Program) => program.channel, (program: Program) => program.start])
|
|
||||||
.groupBy((program: Program) => {
|
const groupedChannels = this.channels
|
||||||
const lang =
|
.map((channel: epgGrabber.Channel) => {
|
||||||
program.titles && program.titles.length && program.titles[0].lang
|
if (channel.xmltv_id && !channel.icon) {
|
||||||
? program.titles[0].lang
|
const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
|
||||||
: 'en'
|
if (foundFeed && foundFeed.hasLogo()) {
|
||||||
|
channel.icon = foundFeed.getLogoUrl()
|
||||||
return pathTemplate.format({ lang, site: program.site || '' })
|
} else {
|
||||||
})
|
const [channelId] = channel.xmltv_id.split('@')
|
||||||
|
const foundChannel: Channel = channelsKeyById.get(channelId)
|
||||||
for (const groupKey of groupedPrograms.keys()) {
|
if (foundChannel && foundChannel.hasLogo()) {
|
||||||
const guide = new Guide({
|
channel.icon = foundChannel.getLogoUrl()
|
||||||
filepath: groupKey,
|
}
|
||||||
gzip: this.options.gzip,
|
}
|
||||||
channels: new Collection(groupedChannels.get(groupKey)),
|
}
|
||||||
programs: new Collection(groupedPrograms.get(groupKey)),
|
|
||||||
logger: this.logger
|
return channel
|
||||||
})
|
})
|
||||||
|
.orderBy([
|
||||||
await guide.save()
|
(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 || '' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupedPrograms = this.programs
|
||||||
|
.orderBy([
|
||||||
|
(program: epgGrabber.Program) => program.channel,
|
||||||
|
(program: epgGrabber.Program) => program.start
|
||||||
|
])
|
||||||
|
.groupBy((program: epgGrabber.Program) => {
|
||||||
|
const lang =
|
||||||
|
program.titles && program.titles.length && program.titles[0].lang
|
||||||
|
? program.titles[0].lang
|
||||||
|
: 'en'
|
||||||
|
|
||||||
|
return pathTemplate.format({ lang, site: program.site || '' })
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const groupKey of groupedPrograms.keys()) {
|
||||||
|
const guide = new Guide({
|
||||||
|
filepath: groupKey,
|
||||||
|
gzip: this.options.gzip,
|
||||||
|
channels: new Collection(groupedChannels.get(groupKey)),
|
||||||
|
programs: new Collection(groupedPrograms.get(groupKey))
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
interface Column {
|
||||||
|
name: string
|
||||||
|
nowrap?: boolean
|
||||||
|
align?: string
|
||||||
|
colspan?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataItem = {
|
||||||
|
value: string
|
||||||
|
nowrap?: boolean
|
||||||
|
align?: string
|
||||||
|
colspan?: number
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export class HTMLTable {
|
||||||
|
data: DataItem[]
|
||||||
|
columns: Column[]
|
||||||
|
|
||||||
|
constructor(data: DataItem[], columns: Column[]) {
|
||||||
|
this.data = data
|
||||||
|
this.columns = columns
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
let output = '<table>\r\n'
|
||||||
|
|
||||||
|
output += ' <thead>\r\n <tr>'
|
||||||
|
for (const column of this.columns) {
|
||||||
|
const nowrap = column.nowrap ? ' nowrap' : ''
|
||||||
|
const align = column.align ? ` align="${column.align}"` : ''
|
||||||
|
const colspan = column.colspan ? ` colspan="${column.colspan}"` : ''
|
||||||
|
|
||||||
|
output += `<th${align}${nowrap}${colspan}>${column.name}</th>`
|
||||||
|
}
|
||||||
|
output += '</tr>\r\n </thead>\r\n'
|
||||||
|
|
||||||
|
output += ' <tbody>\r\n'
|
||||||
|
for (const row of this.data) {
|
||||||
|
output += ' <tr>'
|
||||||
|
for (const item of row) {
|
||||||
|
const nowrap = item.nowrap ? ' nowrap' : ''
|
||||||
|
const align = item.align ? ` align="${item.align}"` : ''
|
||||||
|
const colspan = item.colspan ? ` colspan="${item.colspan}"` : ''
|
||||||
|
|
||||||
|
output += `<td${align}${nowrap}${colspan}>${item.value}</td>`
|
||||||
|
}
|
||||||
|
output += '</tr>\r\n'
|
||||||
|
}
|
||||||
|
output += ' </tbody>\r\n'
|
||||||
|
|
||||||
|
output += '</table>'
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-12
@@ -1,12 +1,14 @@
|
|||||||
export * from './xml'
|
export * from './apiClient'
|
||||||
export * from './channelsParser'
|
export * from './channelsParser'
|
||||||
export * from './xmltv'
|
export * from './configLoader'
|
||||||
export * from './configLoader'
|
export * from './dataLoader'
|
||||||
export * from './grabber'
|
export * from './dataProcessor'
|
||||||
export * from './job'
|
export * from './grabber'
|
||||||
export * from './queue'
|
export * from './guideManager'
|
||||||
export * from './guideManager'
|
export * from './htmlTable'
|
||||||
export * from './guide'
|
export * from './issueLoader'
|
||||||
export * from './apiChannel'
|
export * from './issueParser'
|
||||||
export * from './apiClient'
|
export * from './job'
|
||||||
export * from './queueCreator'
|
export * from './proxyParser'
|
||||||
|
export * from './queue'
|
||||||
|
export * from './queueCreator'
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
|
||||||
|
import { paginateRest } from '@octokit/plugin-paginate-rest'
|
||||||
|
import { TESTING, OWNER, REPO } from '../constants'
|
||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Octokit } from '@octokit/core'
|
||||||
|
import { IssueParser } from './'
|
||||||
|
|
||||||
|
const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
|
||||||
|
const octokit = new CustomOctokit()
|
||||||
|
|
||||||
|
export class IssueLoader {
|
||||||
|
async load(props?: { labels: string[] | string }) {
|
||||||
|
let labels = ''
|
||||||
|
if (props && props.labels) {
|
||||||
|
labels = Array.isArray(props.labels) ? props.labels.join(',') : props.labels
|
||||||
|
}
|
||||||
|
let issues: object[] = []
|
||||||
|
if (TESTING) {
|
||||||
|
issues = (await import('../../tests/__data__/input/sites_update/issues.mjs')).default
|
||||||
|
} else {
|
||||||
|
issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||||
|
owner: OWNER,
|
||||||
|
repo: REPO,
|
||||||
|
per_page: 100,
|
||||||
|
labels,
|
||||||
|
state: 'open',
|
||||||
|
headers: {
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new IssueParser()
|
||||||
|
|
||||||
|
return new Collection(issues).map(parser.parse)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Dictionary } from '@freearhey/core'
|
||||||
|
import { Issue } from '../models'
|
||||||
|
|
||||||
|
const FIELDS = new Dictionary({
|
||||||
|
Site: 'site'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class IssueParser {
|
||||||
|
parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
|
||||||
|
const fields = issue.body.split('###')
|
||||||
|
|
||||||
|
const data = new Dictionary()
|
||||||
|
fields.forEach((field: string) => {
|
||||||
|
const parsed = field.split(/\r?\n/).filter(Boolean)
|
||||||
|
let _label = parsed.shift()
|
||||||
|
_label = _label ? _label.trim() : ''
|
||||||
|
let _value = parsed.join('\r\n')
|
||||||
|
_value = _value ? _value.trim() : ''
|
||||||
|
|
||||||
|
if (!_label || !_value) return data
|
||||||
|
|
||||||
|
const id: string = FIELDS.get(_label)
|
||||||
|
const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
|
||||||
|
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
data.set(id, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const labels = issue.labels.map(label => label.name)
|
||||||
|
|
||||||
|
return new Issue({ number: issue.number, labels, data })
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-34
@@ -1,34 +1,34 @@
|
|||||||
import { Logger } from '@freearhey/core'
|
import { Logger } from '@freearhey/core'
|
||||||
import { Queue, Grabber, GuideManager } from '.'
|
import { Queue, Grabber, GuideManager } from '.'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
|
|
||||||
type JobProps = {
|
interface JobProps {
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
logger: Logger
|
logger: Logger
|
||||||
queue: Queue
|
queue: Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Job {
|
export class Job {
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
logger: Logger
|
logger: Logger
|
||||||
grabber: Grabber
|
grabber: Grabber
|
||||||
|
|
||||||
constructor({ queue, logger, options }: JobProps) {
|
constructor({ queue, logger, options }: JobProps) {
|
||||||
this.options = options
|
this.options = options
|
||||||
this.logger = logger
|
this.logger = logger
|
||||||
this.grabber = new Grabber({ logger, queue, options })
|
this.grabber = new Grabber({ logger, queue, options })
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const { channels, programs } = await this.grabber.grab()
|
const { channels, programs } = await this.grabber.grab()
|
||||||
|
|
||||||
const manager = new GuideManager({
|
const manager = new GuideManager({
|
||||||
channels,
|
channels,
|
||||||
programs,
|
programs,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
logger: this.logger
|
logger: this.logger
|
||||||
})
|
})
|
||||||
|
|
||||||
await manager.createGuides()
|
await manager.createGuides()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { URL } from 'node:url'
|
||||||
|
|
||||||
|
interface ProxyParserResult {
|
||||||
|
protocol: string | null
|
||||||
|
auth?: {
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
host: string
|
||||||
|
port: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProxyParser {
|
||||||
|
parse(_url: string): ProxyParserResult {
|
||||||
|
const parsed = new URL(_url)
|
||||||
|
|
||||||
|
const result: ProxyParserResult = {
|
||||||
|
protocol: parsed.protocol.replace(':', '') || null,
|
||||||
|
host: parsed.hostname,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
-45
@@ -1,45 +1,45 @@
|
|||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import { SiteConfig, Channel } from 'epg-grabber'
|
import { SiteConfig, Channel } from 'epg-grabber'
|
||||||
|
|
||||||
export type QueueItem = {
|
export interface QueueItem {
|
||||||
channel: Channel
|
channel: Channel
|
||||||
date: string
|
date: string
|
||||||
config: SiteConfig
|
config: SiteConfig
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Queue {
|
export class Queue {
|
||||||
_data: Dictionary
|
_data: Dictionary
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = new Dictionary()
|
this._data = new Dictionary()
|
||||||
}
|
}
|
||||||
|
|
||||||
missing(key: string): boolean {
|
missing(key: string): boolean {
|
||||||
return this._data.missing(key)
|
return this._data.missing(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
add(
|
add(
|
||||||
key: string,
|
key: string,
|
||||||
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
|
{ channel, config, date }: { channel: Channel; date: string | null; config: SiteConfig }
|
||||||
) {
|
) {
|
||||||
this._data.set(key, {
|
this._data.set(key, {
|
||||||
channel,
|
channel,
|
||||||
date,
|
date,
|
||||||
config,
|
config,
|
||||||
error: null
|
error: null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
size(): number {
|
size(): number {
|
||||||
return Object.values(this._data.data()).length
|
return Object.values(this._data.data()).length
|
||||||
}
|
}
|
||||||
|
|
||||||
items(): QueueItem[] {
|
items(): QueueItem[] {
|
||||||
return Object.values(this._data.data()) as QueueItem[]
|
return Object.values(this._data.data()) as QueueItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this.size() === 0
|
return this.size() === 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,63 @@
|
|||||||
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
|
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
|
||||||
import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './'
|
import { SITES_DIR, DATA_DIR } from '../constants'
|
||||||
import { SITES_DIR, DATA_DIR } from '../constants'
|
import { GrabOptions } from '../commands/epg/grab'
|
||||||
import { Channel, SiteConfig } from 'epg-grabber'
|
import { ConfigLoader, Queue } from './'
|
||||||
import path from 'path'
|
import { SiteConfig } from 'epg-grabber'
|
||||||
import { GrabOptions } from '../commands/epg/grab'
|
import path from 'path'
|
||||||
|
|
||||||
type QueueCreatorProps = {
|
interface QueueCreatorProps {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
options: GrabOptions
|
options: GrabOptions
|
||||||
parsedChannels: Collection
|
channels: Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueueCreator {
|
export class QueueCreator {
|
||||||
configLoader: ConfigLoader
|
configLoader: ConfigLoader
|
||||||
logger: Logger
|
logger: Logger
|
||||||
sitesStorage: Storage
|
sitesStorage: Storage
|
||||||
dataStorage: Storage
|
dataStorage: Storage
|
||||||
parser: ChannelsParser
|
channels: Collection
|
||||||
parsedChannels: Collection
|
options: GrabOptions
|
||||||
options: GrabOptions
|
|
||||||
|
constructor({ channels, logger, options }: QueueCreatorProps) {
|
||||||
constructor({ parsedChannels, logger, options }: QueueCreatorProps) {
|
this.channels = channels
|
||||||
this.parsedChannels = parsedChannels
|
this.logger = logger
|
||||||
this.logger = logger
|
this.sitesStorage = new Storage()
|
||||||
this.sitesStorage = new Storage()
|
this.dataStorage = new Storage(DATA_DIR)
|
||||||
this.dataStorage = new Storage(DATA_DIR)
|
this.options = options
|
||||||
this.parser = new ChannelsParser({ storage: new Storage() })
|
this.configLoader = new ConfigLoader()
|
||||||
this.options = options
|
}
|
||||||
this.configLoader = new ConfigLoader()
|
|
||||||
}
|
async create(): Promise<Queue> {
|
||||||
|
let index = 0
|
||||||
async create(): Promise<Queue> {
|
const queue = new Queue()
|
||||||
const channelsContent = await this.dataStorage.json('channels.json')
|
for (const channel of this.channels.all()) {
|
||||||
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
|
channel.index = index++
|
||||||
|
if (!channel.site || !channel.site_id || !channel.name) continue
|
||||||
const queue = new Queue()
|
|
||||||
for (const channel of this.parsedChannels.all()) {
|
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
|
||||||
if (!channel.site || !channel.site_id || !channel.name) continue
|
const config: SiteConfig = await this.configLoader.load(configPath)
|
||||||
if (this.options.lang && channel.lang !== this.options.lang) continue
|
|
||||||
|
if (!channel.xmltv_id) {
|
||||||
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
|
channel.xmltv_id = channel.site_id
|
||||||
const config: SiteConfig = await this.configLoader.load(configPath)
|
}
|
||||||
|
|
||||||
if (channel.xmltv_id) {
|
const days = this.options.days || config.days || 1
|
||||||
const found: ApiChannel = channels.first(
|
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString())
|
||||||
(_channel: ApiChannel) => _channel.id === channel.xmltv_id
|
const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd'))
|
||||||
)
|
dates.forEach((date: DateTime) => {
|
||||||
if (found) {
|
const dateString = date.toJSON()
|
||||||
channel.icon = found.logo
|
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
|
||||||
channel.name = found.name
|
if (queue.missing(key)) {
|
||||||
}
|
queue.add(key, {
|
||||||
} else {
|
channel,
|
||||||
channel.xmltv_id = channel.site_id
|
date: dateString,
|
||||||
}
|
config
|
||||||
|
})
|
||||||
const days = this.options.days || config.days || 1
|
}
|
||||||
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString())
|
})
|
||||||
const dates = Array.from({ length: days }, (_, day) => currDate.add(day, 'd'))
|
}
|
||||||
dates.forEach((date: DateTime) => {
|
|
||||||
const dateString = date.toJSON()
|
return queue
|
||||||
const key = `${channel.site}:${channel.lang}:${channel.xmltv_id}:${dateString}`
|
}
|
||||||
if (queue.missing(key)) {
|
}
|
||||||
queue.add(key, {
|
|
||||||
channel,
|
|
||||||
date: dateString,
|
|
||||||
config
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return queue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/\n|\r/g, ' ')
|
|
||||||
.replace(/ +/g, ' ')
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { ChannelData, ChannelSearchableData } from '../types/channel'
|
||||||
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
|
import { Stream, Feed, Logo, GuideChannel } from './'
|
||||||
|
|
||||||
|
export class Channel {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
altNames?: Collection
|
||||||
|
network?: string
|
||||||
|
owners?: Collection
|
||||||
|
countryCode?: string
|
||||||
|
subdivisionCode?: string
|
||||||
|
cityName?: string
|
||||||
|
categoryIds?: Collection
|
||||||
|
isNSFW = false
|
||||||
|
launched?: string
|
||||||
|
closed?: string
|
||||||
|
replacedBy?: string
|
||||||
|
website?: string
|
||||||
|
feeds?: Collection
|
||||||
|
logos: Collection = new Collection()
|
||||||
|
|
||||||
|
constructor(data?: ChannelData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
this.id = data.id
|
||||||
|
this.name = data.name
|
||||||
|
this.altNames = new Collection(data.alt_names)
|
||||||
|
this.network = data.network || undefined
|
||||||
|
this.owners = new Collection(data.owners)
|
||||||
|
this.countryCode = data.country
|
||||||
|
this.subdivisionCode = data.subdivision || undefined
|
||||||
|
this.cityName = data.city || undefined
|
||||||
|
this.categoryIds = new Collection(data.categories)
|
||||||
|
this.isNSFW = data.is_nsfw
|
||||||
|
this.launched = data.launched || undefined
|
||||||
|
this.closed = data.closed || undefined
|
||||||
|
this.replacedBy = data.replaced_by || undefined
|
||||||
|
this.website = data.website || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeeds(): Collection {
|
||||||
|
if (!this.feeds) return new Collection()
|
||||||
|
|
||||||
|
return this.feeds
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuideChannels(): Collection {
|
||||||
|
let channels = new Collection()
|
||||||
|
|
||||||
|
this.getFeeds().forEach((feed: Feed) => {
|
||||||
|
channels = channels.concat(feed.getGuideChannels())
|
||||||
|
})
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuideChannelNames(): Collection {
|
||||||
|
return this.getGuideChannels()
|
||||||
|
.map((channel: GuideChannel) => channel.siteName)
|
||||||
|
.uniq()
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreams(): Collection {
|
||||||
|
let streams = new Collection()
|
||||||
|
|
||||||
|
this.getFeeds().forEach((feed: Feed) => {
|
||||||
|
streams = streams.concat(feed.getStreams())
|
||||||
|
})
|
||||||
|
|
||||||
|
return streams
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreamNames(): Collection {
|
||||||
|
return this.getStreams()
|
||||||
|
.map((stream: Stream) => stream.getName())
|
||||||
|
.uniq()
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeedFullNames(): Collection {
|
||||||
|
return this.getFeeds()
|
||||||
|
.map((feed: Feed) => feed.getFullName())
|
||||||
|
.uniq()
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return this.id || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
getAltNames(): 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: Record<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 {
|
||||||
|
return {
|
||||||
|
id: this.getId(),
|
||||||
|
name: this.getName(),
|
||||||
|
altNames: this.getAltNames().all(),
|
||||||
|
guideNames: this.getGuideChannelNames().all(),
|
||||||
|
streamNames: this.getStreamNames().all(),
|
||||||
|
feedFullNames: this.getFeedFullNames().all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = '') {
|
||||||
|
if (!value) return defaultValue
|
||||||
|
|
||||||
|
const regex = new RegExp(
|
||||||
|
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
|
||||||
|
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
|
||||||
|
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
|
||||||
|
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
|
||||||
|
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
|
||||||
|
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
|
||||||
|
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
|
||||||
|
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
|
||||||
|
'g'
|
||||||
|
)
|
||||||
|
|
||||||
|
value = String(value || '').replace(regex, '')
|
||||||
|
|
||||||
|
return value
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
|
import { FeedData } from '../types/feed'
|
||||||
|
import { Logo, Channel } from '.'
|
||||||
|
|
||||||
|
export class Feed {
|
||||||
|
channelId: string
|
||||||
|
channel?: Channel
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
isMain: boolean
|
||||||
|
broadcastAreaCodes: Collection
|
||||||
|
languageCodes: Collection
|
||||||
|
timezoneIds: Collection
|
||||||
|
videoFormat: string
|
||||||
|
guideChannels?: Collection
|
||||||
|
streams?: Collection
|
||||||
|
logos: Collection = new Collection()
|
||||||
|
|
||||||
|
constructor(data: FeedData) {
|
||||||
|
this.channelId = data.channel
|
||||||
|
this.id = data.id
|
||||||
|
this.name = data.name
|
||||||
|
this.isMain = data.is_main
|
||||||
|
this.broadcastAreaCodes = new Collection(data.broadcast_area)
|
||||||
|
this.languageCodes = new Collection(data.languages)
|
||||||
|
this.timezoneIds = new Collection(data.timezones)
|
||||||
|
this.videoFormat = data.video_format
|
||||||
|
}
|
||||||
|
|
||||||
|
withChannel(channelsKeyById: Dictionary): this {
|
||||||
|
this.channel = channelsKeyById.get(this.channelId)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withStreams(streamsGroupedById: Dictionary): this {
|
||||||
|
this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`))
|
||||||
|
|
||||||
|
if (this.isMain) {
|
||||||
|
this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withGuideChannels(guideChannelsGroupedByStreamId: Dictionary): this {
|
||||||
|
this.guideChannels = new Collection(
|
||||||
|
guideChannelsGroupedByStreamId.get(`${this.channelId}@${this.id}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (this.isMain) {
|
||||||
|
this.guideChannels = this.guideChannels.concat(
|
||||||
|
new Collection(guideChannelsGroupedByStreamId.get(this.channelId))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withLogos(logosGroupedByStreamId: Dictionary): this {
|
||||||
|
this.logos = new Collection(logosGroupedByStreamId.get(this.getStreamId()))
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuideChannels(): Collection {
|
||||||
|
if (!this.guideChannels) return new Collection()
|
||||||
|
|
||||||
|
return this.guideChannels
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreams(): Collection {
|
||||||
|
if (!this.streams) return new Collection()
|
||||||
|
|
||||||
|
return this.streams
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullName(): string {
|
||||||
|
if (!this.channel) return ''
|
||||||
|
|
||||||
|
return `${this.channel.name} ${this.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreamId(): string {
|
||||||
|
return `${this.channelId}@${this.id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogos(): Collection {
|
||||||
|
function format(logo: Logo): number {
|
||||||
|
const levelByFormat: Record<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 || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Collection, DateTime } from '@freearhey/core'
|
||||||
|
import { generateXMLTV } from 'epg-grabber'
|
||||||
|
|
||||||
|
interface GuideData {
|
||||||
|
channels: Collection
|
||||||
|
programs: Collection
|
||||||
|
filepath: string
|
||||||
|
gzip: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Guide {
|
||||||
|
channels: Collection
|
||||||
|
programs: Collection
|
||||||
|
filepath: string
|
||||||
|
gzip: boolean
|
||||||
|
|
||||||
|
constructor({ channels, programs, filepath, gzip }: GuideData) {
|
||||||
|
this.channels = channels
|
||||||
|
this.programs = programs
|
||||||
|
this.filepath = filepath
|
||||||
|
this.gzip = gzip || false
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
|
||||||
|
timezone: 'UTC'
|
||||||
|
})
|
||||||
|
|
||||||
|
return generateXMLTV({
|
||||||
|
channels: this.channels.all(),
|
||||||
|
programs: this.programs.all(),
|
||||||
|
date: currDate.toJSON()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export * from './channel'
|
||||||
|
export * from './feed'
|
||||||
|
export * from './guide'
|
||||||
|
export * from './guideChannel'
|
||||||
|
export * from './issue'
|
||||||
|
export * from './logo'
|
||||||
|
export * from './site'
|
||||||
|
export * from './stream'
|
||||||
|
export * from './channelList'
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Dictionary } from '@freearhey/core'
|
||||||
|
import { OWNER, REPO } from '../constants'
|
||||||
|
|
||||||
|
interface IssueProps {
|
||||||
|
number: number
|
||||||
|
labels: string[]
|
||||||
|
data: Dictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Issue {
|
||||||
|
number: number
|
||||||
|
labels: string[]
|
||||||
|
data: Dictionary
|
||||||
|
|
||||||
|
constructor({ number, labels, data }: IssueProps) {
|
||||||
|
this.number = number
|
||||||
|
this.labels = labels
|
||||||
|
this.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
getURL() {
|
||||||
|
return `https://github.com/${OWNER}/${REPO}/issues/${this.number}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = 0
|
||||||
|
height = 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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
import { Issue } from './'
|
||||||
|
|
||||||
|
enum StatusCode {
|
||||||
|
DOWN = 'down',
|
||||||
|
WARNING = 'warning',
|
||||||
|
OK = 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Status {
|
||||||
|
code: StatusCode
|
||||||
|
emoji: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SiteProps {
|
||||||
|
domain: string
|
||||||
|
totalChannels?: number
|
||||||
|
markedChannels?: number
|
||||||
|
issues: Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Site {
|
||||||
|
domain: string
|
||||||
|
totalChannels: number
|
||||||
|
markedChannels: number
|
||||||
|
issues: Collection
|
||||||
|
|
||||||
|
constructor({ domain, totalChannels = 0, markedChannels = 0, issues }: SiteProps) {
|
||||||
|
this.domain = domain
|
||||||
|
this.totalChannels = totalChannels
|
||||||
|
this.markedChannels = markedChannels
|
||||||
|
this.issues = issues
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus(): Status {
|
||||||
|
const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
|
||||||
|
issue.labels.find(label => label === 'status:down')
|
||||||
|
)
|
||||||
|
if (issuesWithStatusDown.notEmpty())
|
||||||
|
return {
|
||||||
|
code: StatusCode.DOWN,
|
||||||
|
emoji: '🔴'
|
||||||
|
}
|
||||||
|
|
||||||
|
const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
|
||||||
|
issue.labels.find(label => label === 'status:warning')
|
||||||
|
)
|
||||||
|
if (issuesWithStatusWarning.notEmpty())
|
||||||
|
return {
|
||||||
|
code: StatusCode.WARNING,
|
||||||
|
emoji: '🟡'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: StatusCode.OK,
|
||||||
|
emoji: '🟢'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIssues(): Collection {
|
||||||
|
return this.issues.map((issue: Issue) => issue.getURL())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { StreamData } from '../types/stream'
|
||||||
|
import { Feed, Channel } from './index'
|
||||||
|
|
||||||
|
export class Stream {
|
||||||
|
name?: string
|
||||||
|
url: string
|
||||||
|
id?: string
|
||||||
|
channelId?: string
|
||||||
|
channel?: Channel
|
||||||
|
feedId?: string
|
||||||
|
feed?: Feed
|
||||||
|
filepath?: string
|
||||||
|
line?: number
|
||||||
|
label?: string
|
||||||
|
verticalResolution?: number
|
||||||
|
isInterlaced?: boolean
|
||||||
|
referrer?: string
|
||||||
|
userAgent?: string
|
||||||
|
groupTitle = 'Undefined'
|
||||||
|
removed = false
|
||||||
|
|
||||||
|
constructor(data: StreamData) {
|
||||||
|
const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel
|
||||||
|
const { verticalResolution, isInterlaced } = parseQuality(data.quality)
|
||||||
|
|
||||||
|
this.id = id || undefined
|
||||||
|
this.channelId = data.channel || undefined
|
||||||
|
this.feedId = data.feed || undefined
|
||||||
|
this.name = data.name || undefined
|
||||||
|
this.url = data.url
|
||||||
|
this.referrer = data.referrer || undefined
|
||||||
|
this.userAgent = data.user_agent || undefined
|
||||||
|
this.verticalResolution = verticalResolution || undefined
|
||||||
|
this.isInterlaced = isInterlaced || undefined
|
||||||
|
this.label = data.label || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return this.id || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseQuality(quality: string | null): {
|
||||||
|
verticalResolution: number | null
|
||||||
|
isInterlaced: boolean | null
|
||||||
|
} {
|
||||||
|
if (!quality) return { verticalResolution: null, isInterlaced: null }
|
||||||
|
const [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
|
||||||
|
const isInterlaced = /i$/i.test(quality)
|
||||||
|
let verticalResolution = 0
|
||||||
|
if (verticalResolutionString) verticalResolution = parseInt(verticalResolutionString)
|
||||||
|
|
||||||
|
return { verticalResolution, isInterlaced }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
site: '<DOMAIN>',
|
||||||
|
url({ channel, date }) {
|
||||||
|
return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}`
|
||||||
|
},
|
||||||
|
parser({ content }) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channels() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# <DOMAIN>
|
||||||
|
|
||||||
|
https://<DOMAIN>
|
||||||
|
|
||||||
|
### Download the guide
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run grab --- --site=<DOMAIN>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update channel list
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run channels:parse --- --config=./sites/<DOMAIN>/<DOMAIN>.config.js --output=./sites/<DOMAIN>/<DOMAIN>.channels.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm test --- <DOMAIN>
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Sites
|
||||||
|
|
||||||
|
_TABLE_
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const { parser, url } = require('./<DOMAIN>.config.js')
|
||||||
|
const dayjs = require('dayjs')
|
||||||
|
const utc = require('dayjs/plugin/utc')
|
||||||
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
|
dayjs.extend(utc)
|
||||||
|
|
||||||
|
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
|
||||||
|
const channel = { site_id: 'bbc1' }
|
||||||
|
|
||||||
|
it('can generate valid url', () => {
|
||||||
|
expect(url({ channel, date })).toBe('https://example.com/api/bbc1/2025-01-12')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can parse response', () => {
|
||||||
|
const content =
|
||||||
|
'[{"title":"Program 1","start":"2025-01-12T00:00:00.000Z","stop":"2025-01-12T00:30:00.000Z"},{"title":"Program 2","start":"2025-01-12T00:30:00.000Z","stop":"2025-01-12T01:00:00.000Z"}]'
|
||||||
|
|
||||||
|
const results = parser({ content })
|
||||||
|
|
||||||
|
expect(results.length).toBe(2)
|
||||||
|
expect(results[0]).toMatchObject({
|
||||||
|
title: 'Program 1',
|
||||||
|
start: '2025-01-12T00:00:00.000Z',
|
||||||
|
stop: '2025-01-12T00:30:00.000Z'
|
||||||
|
})
|
||||||
|
expect(results[1]).toMatchObject({
|
||||||
|
title: 'Program 2',
|
||||||
|
start: '2025-01-12T00:30:00.000Z',
|
||||||
|
stop: '2025-01-12T01:00:00.000Z'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can handle empty guide', () => {
|
||||||
|
const results = parser({ content: '' })
|
||||||
|
|
||||||
|
expect(results).toMatchObject([])
|
||||||
|
})
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
|
||||||
|
export interface ChannelData {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
alt_names: string[]
|
||||||
|
network: string
|
||||||
|
owners: Collection
|
||||||
|
country: string
|
||||||
|
subdivision: string
|
||||||
|
city: string
|
||||||
|
categories: Collection
|
||||||
|
is_nsfw: boolean
|
||||||
|
launched: string
|
||||||
|
closed: string
|
||||||
|
replaced_by: string
|
||||||
|
website: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelSearchableData {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
altNames: string[]
|
||||||
|
guideNames: string[]
|
||||||
|
streamNames: string[]
|
||||||
|
feedFullNames: string[]
|
||||||
|
}
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { Storage } from '@freearhey/core'
|
||||||
|
|
||||||
|
export interface DataLoaderProps {
|
||||||
|
storage: Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataLoaderData {
|
||||||
|
countries: object | object[]
|
||||||
|
regions: object | object[]
|
||||||
|
subdivisions: object | object[]
|
||||||
|
languages: object | object[]
|
||||||
|
categories: object | object[]
|
||||||
|
blocklist: object | object[]
|
||||||
|
channels: object | object[]
|
||||||
|
feeds: object | object[]
|
||||||
|
timezones: object | object[]
|
||||||
|
guides: object | object[]
|
||||||
|
streams: object | object[]
|
||||||
|
logos: object | object[]
|
||||||
|
}
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
|
|
||||||
|
export interface DataProcessorData {
|
||||||
|
guideChannelsGroupedByStreamId: Dictionary
|
||||||
|
feedsGroupedByChannelId: Dictionary
|
||||||
|
logosGroupedByChannelId: Dictionary
|
||||||
|
logosGroupedByStreamId: Dictionary
|
||||||
|
feedsKeyByStreamId: Dictionary
|
||||||
|
streamsGroupedById: Dictionary
|
||||||
|
channelsKeyById: Dictionary
|
||||||
|
guideChannels: Collection
|
||||||
|
channels: Collection
|
||||||
|
streams: Collection
|
||||||
|
feeds: Collection
|
||||||
|
logos: Collection
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
|
||||||
|
export interface FeedData {
|
||||||
|
channel: string
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
is_main: boolean
|
||||||
|
broadcast_area: Collection
|
||||||
|
languages: Collection
|
||||||
|
timezones: Collection
|
||||||
|
video_format: string
|
||||||
|
}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
export interface GuideData {
|
||||||
|
channel: string
|
||||||
|
feed: string
|
||||||
|
site: string
|
||||||
|
site_id: string
|
||||||
|
site_name: string
|
||||||
|
lang: string
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
declare module 'langs'
|
declare module 'langs'
|
||||||
|
|||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
export interface LogoData {
|
||||||
|
channel: string
|
||||||
|
feed: string | null
|
||||||
|
tags: string[]
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
format: string | null
|
||||||
|
url: string
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
export interface StreamData {
|
||||||
|
channel: string | null
|
||||||
|
feed: string | null
|
||||||
|
name?: string
|
||||||
|
url: string
|
||||||
|
referrer: string | null
|
||||||
|
user_agent: string | null
|
||||||
|
quality: string | null
|
||||||
|
label: string | null
|
||||||
|
}
|
||||||
@@ -1,69 +1,69 @@
|
|||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: '9tv.co.il',
|
site: '9tv.co.il',
|
||||||
days: 2,
|
days: 2,
|
||||||
url: function ({ date }) {
|
url: function ({ date }) {
|
||||||
return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format(
|
return `https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=${date.format(
|
||||||
'DD/MM/YYYY 00:00:00'
|
'DD/MM/YYYY 00:00:00'
|
||||||
)}`
|
)}`
|
||||||
},
|
},
|
||||||
parser: function ({ content, date }) {
|
parser: function ({ content, date }) {
|
||||||
const programs = []
|
const programs = []
|
||||||
const items = parseItems(content)
|
const items = parseItems(content)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const prev = programs[programs.length - 1]
|
const prev = programs[programs.length - 1]
|
||||||
const $item = cheerio.load(item)
|
const $item = cheerio.load(item)
|
||||||
const start = parseStart($item, date)
|
const start = parseStart($item, date)
|
||||||
if (prev) prev.stop = start
|
if (prev) prev.stop = start
|
||||||
const stop = start.add(1, 'h')
|
const stop = start.add(1, 'h')
|
||||||
programs.push({
|
programs.push({
|
||||||
title: parseTitle($item),
|
title: parseTitle($item),
|
||||||
image: parseImage($item),
|
image: parseImage($item),
|
||||||
description: parseDescription($item),
|
description: parseDescription($item),
|
||||||
start,
|
start,
|
||||||
stop
|
stop
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStart($item, date) {
|
function parseStart($item, date) {
|
||||||
let time = $item('a > div.guide_list_time').text().trim()
|
let time = $item('a > div.guide_list_time').text().trim()
|
||||||
|
|
||||||
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem')
|
return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Jerusalem')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseImage($item) {
|
function parseImage($item) {
|
||||||
const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css(
|
const backgroundImage = $item('a > div.guide_info_group > div.guide_info_pict').css(
|
||||||
'background-image'
|
'background-image'
|
||||||
)
|
)
|
||||||
if (!backgroundImage) return null
|
if (!backgroundImage) return null
|
||||||
const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null]
|
const [, relativePath] = backgroundImage.match(/url\((.*)\)/) || [null, null]
|
||||||
|
|
||||||
return relativePath ? `https://www.9tv.co.il${relativePath}` : null
|
return relativePath ? `https://www.9tv.co.il${relativePath}` : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDescription($item) {
|
function parseDescription($item) {
|
||||||
return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim()
|
return $item('a > div.guide_info_group > div.guide_txt_group > div').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTitle($item) {
|
function parseTitle($item) {
|
||||||
return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim()
|
return $item('a > div.guide_info_group > div.guide_txt_group > h3').text().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(content) {
|
function parseItems(content) {
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
|
|
||||||
return $('li').toArray()
|
return $('li').toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,56 @@
|
|||||||
const { parser, url } = require('./9tv.co.il.config.js')
|
const { parser, url } = require('./9tv.co.il.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const dayjs = require('dayjs')
|
||||||
dayjs.extend(customParseFormat)
|
const utc = require('dayjs/plugin/utc')
|
||||||
dayjs.extend(utc)
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
dayjs.extend(customParseFormat)
|
||||||
const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: '#',
|
const date = dayjs.utc('2022-03-06', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'Channel9.il'
|
const channel = {
|
||||||
}
|
site_id: '#',
|
||||||
|
xmltv_id: 'Channel9.il'
|
||||||
it('can generate valid url', () => {
|
}
|
||||||
expect(url({ date })).toBe(
|
|
||||||
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
|
it('can generate valid url', () => {
|
||||||
)
|
expect(url({ date })).toBe(
|
||||||
})
|
'https://www.9tv.co.il/BroadcastSchedule/getBrodcastSchedule?date=06/03/2022 00:00:00'
|
||||||
|
)
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. </div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>'
|
it('can parse response', () => {
|
||||||
const result = parser({ content, date }).map(p => {
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
|
||||||
p.start = p.start.toJSON()
|
const result = parser({ content, date }).map(p => {
|
||||||
p.stop = p.stop.toJSON()
|
p.start = p.start.toJSON()
|
||||||
return p
|
p.stop = p.stop.toJSON()
|
||||||
})
|
return p
|
||||||
|
})
|
||||||
expect(result).toMatchObject([
|
|
||||||
{
|
expect(result).toMatchObject([
|
||||||
start: '2022-03-06T04:30:00.000Z',
|
{
|
||||||
stop: '2022-03-06T07:10:00.000Z',
|
start: '2022-03-06T04:30:00.000Z',
|
||||||
title: 'Слепая',
|
stop: '2022-03-06T07:10:00.000Z',
|
||||||
image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
|
title: 'Слепая',
|
||||||
description:
|
image: 'https://www.9tv.co.il/download/pictures/img_id=8484.jpg',
|
||||||
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
|
description:
|
||||||
},
|
'Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.'
|
||||||
{
|
},
|
||||||
start: '2022-03-06T07:10:00.000Z',
|
{
|
||||||
stop: '2022-03-06T08:10:00.000Z',
|
start: '2022-03-06T07:10:00.000Z',
|
||||||
image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
|
stop: '2022-03-06T08:10:00.000Z',
|
||||||
title: 'Орел и решка. Морской сезон',
|
image: 'https://www.9tv.co.il/download/pictures/img_id=23694.jpg',
|
||||||
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
|
title: 'Орел и решка. Морской сезон',
|
||||||
}
|
description: 'Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.'
|
||||||
])
|
}
|
||||||
})
|
])
|
||||||
|
})
|
||||||
it('can handle empty guide', () => {
|
|
||||||
const result = parser({
|
it('can handle empty guide', () => {
|
||||||
date,
|
const result = parser({
|
||||||
channel,
|
date,
|
||||||
content: '<!DOCTYPE html><html><head></head><body></body></html>'
|
channel,
|
||||||
})
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
|
||||||
expect(result).toMatchObject([])
|
})
|
||||||
})
|
expect(result).toMatchObject([])
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы. </div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html><head></head><body></body></html>
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Error
|
||||||
|
><code>NoSuchKey</code><Message>The specified key does not exist.</Message
|
||||||
|
><Key>processed/Sydney_2026-02-04.json</Key><RequestId>A8BGG30W2N9P97Z1</RequestId
|
||||||
|
><HostId
|
||||||
|
>4NlYzMSR9t2HPBctGhbCKPEBuFdVxtbuyLIOA7uTL28R6/ooqneS1TTwVa7PkTt7gr9yyopqCKk=</HostId
|
||||||
|
></Error
|
||||||
|
>
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<channels>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SPREE">SPREE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#TENHD">TENHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7HD">7HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9">9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9HD">9HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9Rush">9Rush</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SPREE">SPREE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#TENHD">TENHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ASPIR">ASPIR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#PrimS">PrimS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SCA9">SCA9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SCHD">SCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SNR">SNR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#9">9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#9HD">9HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ASPIR">ASPIR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#DDT">DDT</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SC">SC</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7HD">7HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#9">9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#9HD">9HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#10Nth">10Nth</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#NBN">NBN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#Prime">Prime</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SNR">SNR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SC">SC</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SCA9">SCA9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SCHD">SCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SHD7">SHD7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#TDHD">TDHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#TDT">TDT</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7HD">7HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9">9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9HD">9HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9Rush">9Rush</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SPREE">SPREE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#TENHD">TENHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7GTS">7GTS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#10Nth">10Nth</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ASPIR">ASPIR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#Imp">Imp</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#NBN">NBN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#PrimN">PrimN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#PrimS">PrimS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SCA9">SCA9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SCHD">SCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SNR">SNR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#7C">7C</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#Imp">Imp</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7HD">7HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9">9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9HD">9HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9Rush">9Rush</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SPREE">SPREE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#TENHD">TENHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7C">7C</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7QLD">7QLD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7TWOQ">7TWOQ</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ASPIR">ASPIR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#Imp">Imp</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SCA9">SCA9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SCHD">SCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SNR">SNR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7C">7C</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7GTS">7GTS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ASPIR">ASPIR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#Imp">Imp</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SC9">SC9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SNR">SNR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WIN9">WIN9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WIN10">WIN10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#ASPIR">ASPIR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SC">SC</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SCA9">SCA9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SCHD">SCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#SHD7">SHD7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#TDHD">TDHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#TDT">TDT</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Tasmania#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SCA9">SCA9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SCHD">SCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Townsville#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#ASPIR">ASPIR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#Prime">Prime</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SCA9">SCA9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SCHD">SCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#SNR">SNR</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#TVSN">TVSN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Victoria#WinSA">WinSA</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#GWN">GWN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#WDT">WDT</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#WIN">WIN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Western Australia#WINHD">WINHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7bravoAdelaide.au" site_id="Adelaide#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7bravoSydney.au" site_id="Sydney#7bravo">7bravo</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7flixAdelaide.au" site_id="Adelaide#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7flixSydney.au" site_id="Sydney#7flix">7flix</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7HDAdelaide.au" site_id="Adelaide#7HD">7HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7mateAdelaide.au" site_id="Adelaide#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7mateSydney.au" site_id="Sydney#7MATE">7MATE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7twoAdelaide.au" site_id="Adelaide#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="7twoSydney.au" site_id="Sydney#7TWO">7TWO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="9GemSydney.au" site_id="Sydney#GEM">GEM</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="9GoSydney.au" site_id="Sydney#GO">GO</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="9HDAdelaide.au" site_id="Adelaide#9HD">9HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="9LifeAdelaide.au" site_id="Adelaide#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="9LifeSydney.au" site_id="Sydney#9Life">9Life</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="9RushAdelaide.au" site_id="Adelaide#9Rush">9Rush</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="9RushSydney.au" site_id="Sydney#9Rush">9Rush</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="10BoldSydney.au" site_id="Sydney#ONE">ONE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="10PeachSydney.au" site_id="Sydney#11">11</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="10ShakeSydney.au" site_id="Sydney#SHAKE">SHAKE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCAustralia.au" site_id="Sydney#ABCN">ABCN</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCKidsSydney.au" site_id="Sydney#ABC4KIDS">ABC4KIDS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCMESydney.au" site_id="Sydney#ABC3">ABC3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVNSW.au" site_id="New South Wales#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVNT.au" site_id="Northern Territory#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVPlusSydney.au" site_id="Sydney#ABC2">ABC2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVQLD.au" site_id="Queensland#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVSA.au" site_id="South Australia#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVSydney.au" site_id="Sydney#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVSydney.au" site_id="Sydney#ABCHD">ABCHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVTAS.au" site_id="Tasmania#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVVIC.au" site_id="Victoria#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="ABCTVWA.au" site_id="Western Australia#ABC1">ABC1</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="C31Melbourne.au" site_id="Melbourne#31">31</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Channel7Adelaide.au" site_id="Adelaide#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Channel7Sydney.au" site_id="Sydney#7">7</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Channel7Sydney.au" site_id="Sydney#7HD">7HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Channel9Adelaide.au" site_id="Adelaide#9">9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Channel9Sydney.au" site_id="Sydney#9">9</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Channel9Sydney.au" site_id="Sydney#9HD">9HD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="10Adelaide.au" site_id="Adelaide#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="10Sydney.au" site_id="Sydney#10">10</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="10Sydney.au" site_id="Sydney#TENHD">TENHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Channel44.au" site_id="Adelaide#44">44</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="NITVAdelaide.au" site_id="Adelaide#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="NITVSydney.au" site_id="Sydney#NITV">NITV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="Racingcom.au" site_id="Sydney#RTV">RTV</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SBSAdelaide.au" site_id="Adelaide#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SBSFoodSydney.au" site_id="Sydney#SBS3">SBS3</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SBSSydney.au" site_id="Sydney#SBS">SBS</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SBSSydney.au" site_id="Sydney#SBSHD">SBSHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SBSVicelandSydney.au" site_id="Sydney#VICHD">VICHD</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SBSWorldMoviesSydney.au" site_id="Sydney#SBS2">SBS2</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SBSWorldWatchSydney.au" site_id="Sydney#SBSWW">SBSWW</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="SpreeTV.au" site_id="Sydney#SPREE">SPREE</channel>
|
|
||||||
<channel site="abc.net.au" lang="en" xmltv_id="TVSN.au" site_id="Sydney#TVSN">TVSN</channel>
|
|
||||||
</channels>
|
|
||||||
@@ -1,121 +1,122 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
site: 'abc.net.au',
|
site: 'abc.net.au',
|
||||||
days: 3,
|
days: 3,
|
||||||
request: {
|
request: {
|
||||||
cache: {
|
cache: {
|
||||||
ttl: 60 * 60 * 1000 // 1 hour
|
ttl: 60 * 60 * 1000 // 1 hour
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
url({ date, channel }) {
|
url({ date, channel }) {
|
||||||
const [region] = channel.site_id.split('#')
|
const [region] = channel.site_id.split('#')
|
||||||
|
|
||||||
return `https://epg.abctv.net.au/processed/${region}_${date.format('YYYY-MM-DD')}.json`
|
return `https://cdn.iview.abc.net.au/epg/processed/${region}_${date.format('YYYY-MM-DD')}.json`
|
||||||
},
|
},
|
||||||
parser({ content, channel }) {
|
parser({ content, channel }) {
|
||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
sub_title: item.episode_title,
|
sub_title: item.episode_title,
|
||||||
category: item.genres,
|
category: item.genres,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
season: parseSeason(item),
|
season: parseSeason(item),
|
||||||
episode: parseEpisode(item),
|
episode: parseEpisode(item),
|
||||||
rating: parseRating(item),
|
rating: parseRating(item),
|
||||||
image: parseImage(item),
|
image: parseImage(item),
|
||||||
start: parseTime(item.start_time),
|
start: parseTime(item.start_time),
|
||||||
stop: parseTime(item.end_time)
|
stop: parseTime(item.end_time)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
async channels() {
|
async channels({ region = 'syd' }) {
|
||||||
const now = dayjs()
|
const now = dayjs()
|
||||||
const regions = [
|
const regions = {
|
||||||
'Sydney',
|
syd: 'Sydney',
|
||||||
'Melbourne',
|
mel: 'Melbourne',
|
||||||
'Brisbane',
|
bri: 'Brisbane',
|
||||||
'GoldCoast',
|
gc: 'GoldCoast',
|
||||||
'Perth',
|
per: 'Perth',
|
||||||
'Adelaide',
|
adl: 'Adelaide',
|
||||||
'Hobart',
|
hbr: 'Hobart',
|
||||||
'Darwin',
|
drw: 'Darwin',
|
||||||
'Canberra',
|
cbr: 'Canberra',
|
||||||
'New South Wales',
|
nsw: 'New South Wales',
|
||||||
'Victoria',
|
vic: 'Victoria',
|
||||||
'Townsville',
|
tsv: 'Townsville',
|
||||||
'Queensland',
|
qld: 'Queensland',
|
||||||
'Western Australia',
|
wa: 'Western Australia',
|
||||||
'South Australia',
|
sa: 'South Australia',
|
||||||
'Tasmania',
|
tas: 'Tasmania',
|
||||||
'Northern Territory'
|
nt: 'Northern Territory'
|
||||||
]
|
}
|
||||||
|
|
||||||
let channels = []
|
let channels = []
|
||||||
for (let region of regions) {
|
const regionName = regions[region]
|
||||||
const data = await axios
|
const data = await axios
|
||||||
.get(`https://epg.abctv.net.au/processed/${region}_${now.format('YYYY-MM-DD')}.json`)
|
.get(
|
||||||
.then(r => r.data)
|
`https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json`
|
||||||
.catch(console.log)
|
)
|
||||||
|
.then(r => r.data)
|
||||||
for (let item of data.schedule) {
|
.catch(console.log)
|
||||||
channels.push({
|
|
||||||
lang: 'en',
|
for (let item of data.schedule) {
|
||||||
site_id: `${region}#${item.channel}`,
|
channels.push({
|
||||||
name: item.channel
|
lang: 'en',
|
||||||
})
|
site_id: `${regionName}#${item.channel}`,
|
||||||
}
|
name: item.channel
|
||||||
}
|
})
|
||||||
|
}
|
||||||
return channels
|
|
||||||
}
|
return channels
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function parseItems(content, channel) {
|
|
||||||
try {
|
function parseItems(content, channel) {
|
||||||
const data = JSON.parse(content)
|
try {
|
||||||
if (!data) return []
|
const data = JSON.parse(content)
|
||||||
if (!Array.isArray(data.schedule)) return []
|
if (!data) return []
|
||||||
|
if (!Array.isArray(data.schedule)) return []
|
||||||
const [, channelId] = channel.site_id.split('#')
|
|
||||||
const channelData = data.schedule.find(i => i.channel == channelId)
|
const [, channelId] = channel.site_id.split('#')
|
||||||
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
|
const channelData = data.schedule.find(i => i.channel == channelId)
|
||||||
} catch (err) {
|
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
|
||||||
return []
|
} catch {
|
||||||
}
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function parseSeason(item) {
|
|
||||||
return item.series_num || null
|
function parseSeason(item) {
|
||||||
}
|
return item.series_num || null
|
||||||
function parseEpisode(item) {
|
}
|
||||||
return item.episode_num || null
|
function parseEpisode(item) {
|
||||||
}
|
return item.episode_num || null
|
||||||
function parseTime(time) {
|
}
|
||||||
return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney')
|
function parseTime(time) {
|
||||||
}
|
return dayjs.tz(time, 'YYYY-MM-DD HH:mm', 'Australia/Sydney')
|
||||||
function parseImage(item) {
|
}
|
||||||
return item.image_file
|
function parseImage(item) {
|
||||||
? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}`
|
return item.image_file
|
||||||
: null
|
? `https://www.abc.net.au/tv/common/images/publicity/${item.image_file}`
|
||||||
}
|
: null
|
||||||
function parseRating(item) {
|
}
|
||||||
return item.rating
|
function parseRating(item) {
|
||||||
? {
|
return item.rating
|
||||||
system: 'ACB',
|
? {
|
||||||
value: item.rating
|
system: 'ACB',
|
||||||
}
|
value: item.rating
|
||||||
: null
|
}
|
||||||
}
|
: null
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,54 +1,51 @@
|
|||||||
const { parser, url } = require('./abc.net.au.config.js')
|
const { parser, url } = require('./abc.net.au.config.js')
|
||||||
const dayjs = require('dayjs')
|
const fs = require('fs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const path = require('path')
|
||||||
dayjs.extend(utc)
|
const dayjs = require('dayjs')
|
||||||
|
const utc = require('dayjs/plugin/utc')
|
||||||
const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d')
|
dayjs.extend(utc)
|
||||||
const channel = {
|
|
||||||
site_id: 'Sydney#ABC1',
|
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
|
||||||
xmltv_id: 'ABCTV.au'
|
const channel = { site_id: 'Sydney#ABC1' }
|
||||||
}
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ date, channel })).toBe('https://epg.abctv.net.au/processed/Sydney_2022-12-22.json')
|
expect(url({ date, channel })).toBe(
|
||||||
})
|
'https://cdn.iview.abc.net.au/epg/processed/Sydney_2025-02-04.json'
|
||||||
|
)
|
||||||
it('can parse response', () => {
|
})
|
||||||
const content =
|
|
||||||
'{"date":"2022-12-22","region":"Sydney","schedule":[{"channel":"ABC1","listing":[{"consumer_advice":"Adult Themes, Drug Use, Violence","rating":"M","show_id":912747,"repeat":true,"description":"When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?","title":"Silent Witness","crid":"ZW2178A004S00","start_time":"2022-12-22T00:46:00","series-crid":"ZW2178A","live":false,"captioning":true,"show_type":"Episode","series_num":22,"episode_title":"Lift Up Your Hearts (part Two)","length":58,"onair_title":"Silent Witness","end_time":"2022-12-22T01:44:00","genres":["Entertainment"],"image_file":"ZW2178A004S00_460.jpg","prog_slug":"silent-witness","episode_num":4}]}]}'
|
it('can parse response', () => {
|
||||||
|
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
|
||||||
const result = parser({ content, channel }).map(p => {
|
const results = parser({ content, channel }).map(p => {
|
||||||
p.start = p.start.toJSON()
|
p.start = p.start.toJSON()
|
||||||
p.stop = p.stop.toJSON()
|
p.stop = p.stop.toJSON()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result).toMatchObject([
|
expect(results.length).toBe(30)
|
||||||
{
|
expect(results[0]).toMatchObject({
|
||||||
title: 'Silent Witness',
|
title: "Julia Zemiro's Home Delivery",
|
||||||
sub_title: 'Lift Up Your Hearts (part Two)',
|
sub_title: 'Maggie Beer',
|
||||||
description:
|
description:
|
||||||
'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?',
|
"The kitchen Maggie Beer made famous in The Cook and the Chef may be in the heart of the Barossa Valley, but our most beloved foodie meets up with Julia where she grew up in Sydney's Lakemba.",
|
||||||
category: ['Entertainment'],
|
category: ['Entertainment', 'Factual'],
|
||||||
rating: {
|
rating: {
|
||||||
system: 'ACB',
|
system: 'ACB',
|
||||||
value: 'M'
|
value: 'G'
|
||||||
},
|
},
|
||||||
season: 22,
|
season: null,
|
||||||
episode: 4,
|
episode: null,
|
||||||
image: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg',
|
image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg',
|
||||||
start: '2022-12-21T13:46:00.000Z',
|
start: '2025-02-03T12:40:00.000Z',
|
||||||
stop: '2022-12-21T14:44:00.000Z'
|
stop: '2025-02-03T13:09:00.000Z'
|
||||||
}
|
})
|
||||||
])
|
})
|
||||||
})
|
|
||||||
|
it('can handle empty guide', () => {
|
||||||
it('can handle empty guide', () => {
|
const results = parser({
|
||||||
const result = parser(
|
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')),
|
||||||
{
|
channel
|
||||||
content:
|
})
|
||||||
'<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>processed/Sydney_2023-01-17.json</Key><RequestId>6MRHX5TJ12X39B3Y</RequestId><HostId>59rH6XRMrmkFywg8Kv58iqpI6O1fuOCuEbKa1HRRYa4buByXMBTvAhz8zuAK7X5D+ZN9ZuWxyGs=</HostId></Error>'
|
|
||||||
},
|
expect(results).toMatchObject([])
|
||||||
channel
|
})
|
||||||
)
|
|
||||||
expect(result).toMatchObject([])
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#7">7</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#7HD">7HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#9">9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#9HD">9HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#9Rush">9Rush</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#44">44</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#SPREE">SPREE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#TENHD">TENHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Adelaide#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7">7</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7HD">7HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9">9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9HD">9HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#9Rush">9Rush</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#SPREE">SPREE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#TENHD">TENHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Brisbane#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ASPIR">ASPIR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#PrimS">PrimS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SCA9">SCA9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SCHD">SCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#SNR">SNR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#WIN">WIN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Canberra#WINHD">WINHD</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#9">9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#9HD">9HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ASPIR">ASPIR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#DDT">DDT</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#SC">SC</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Darwin#VICHD">VICHD</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#9">9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#9HD">9HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#10Nth">10Nth</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#NBN">NBN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#Prime">Prime</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#SNR">SNR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="GoldCoast#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SC">SC</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SCA9">SCA9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SCHD">SCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#SHD7">SHD7</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#TDHD">TDHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#TDT">TDT</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#WIN">WIN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#WINHD">WINHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Hobart#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7">7</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7HD">7HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9">9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9HD">9HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#9Rush">9Rush</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#31">31</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#SPREE">SPREE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#TENHD">TENHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Melbourne#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7">7</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7GTS">7GTS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#10Nth">10Nth</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ASPIR">ASPIR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#Imp">Imp</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#NBN">NBN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#PrimN">PrimN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#PrimS">PrimS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SCA9">SCA9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SCHD">SCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#SNR">SNR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#WIN">WIN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#WINHD">WINHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="New South Wales#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#7C">7C</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#Imp">Imp</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Northern Territory#VICHD">VICHD</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7">7</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7HD">7HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9">9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9HD">9HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#9Rush">9Rush</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#SPREE">SPREE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#TENHD">TENHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Perth#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7C">7C</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7QLD">7QLD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#7TWOQ">7TWOQ</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ASPIR">ASPIR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#Imp">Imp</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SCA9">SCA9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SCHD">SCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#SNR">SNR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#WIN">WIN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Queensland#WINHD">WINHD</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7C">7C</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7GTS">7GTS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ASPIR">ASPIR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#Imp">Imp</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SC9">SC9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#SNR">SNR</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WIN">WIN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WIN9">WIN9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WIN10">WIN10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="South Australia#WINHD">WINHD</channel>
|
||||||
|
</channels>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<channels>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#7">7</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#7bravo">7bravo</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#7flix">7flix</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#7HD">7HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#7MATE">7MATE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#7TWO">7TWO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#9">9</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#9HD">9HD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#9Life">9Life</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#9Rush">9Rush</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#10">10</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#11">11</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#ABC1">ABC1</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#ABC2">ABC2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#ABC3">ABC3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#ABC4KIDS">ABC4KIDS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#ABCHD">ABCHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#ABCN">ABCN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#GEM">GEM</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#GO">GO</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#NITV">NITV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#ONE">ONE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#RTV">RTV</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#SBS">SBS</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#SBS2">SBS2</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#SBS3">SBS3</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#SBSHD">SBSHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#SBSWW">SBSWW</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#SHAKE">SHAKE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#SPREE">SPREE</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#TENHD">TENHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#TVSN">TVSN</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#VICHD">VICHD</channel>
|
||||||
|
<channel site="abc.net.au" lang="en" xmltv_id="" site_id="Sydney#YOUTV">YOUTV</channel>
|
||||||
|
</channels>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user