Merge branch 'master' into zap2it.com

This commit is contained in:
Newbiect
2025-08-05 14:28:02 -04:00
committed by GitHub
1309 changed files with 263503 additions and 106197 deletions
-39
View File
@@ -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"
]
}
}
+2
View File
@@ -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
-3
View File
@@ -1,8 +1,5 @@
blank_issues_enabled: true
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
url: https://github.com/orgs/iptv-org/discussions/categories/q-a
about: Ask questions about this project
+53
View File
@@ -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 }}
+17 -5
View File
@@ -7,14 +7,14 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: tibdex/github-app-token@v1.8.2
if: ${{ !env.ACT }}
id: create-app-token
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v3
- uses: actions/checkout@v4
if: ${{ !env.ACT }}
with:
token: ${{ steps.create-app-token.outputs.token }}
@@ -22,16 +22,28 @@ jobs:
run: |
git config user.name "iptv-bot[bot]"
git config user.email "84861620+iptv-bot[bot]@users.noreply.github.com"
- uses: actions/setup-node@v3
if: ${{ !env.ACT }}
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: 'npm'
- name: install dependencies
run: npm install
- name: check *.channels.xml
run: |
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
run: npm run api:generate
- run: git status
+7
View File
@@ -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
+7
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
.husky/scripts_lint.sh
.husky/channels_lint.sh
.husky/channels_validate.sh
+7
View File
@@ -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
View File
@@ -1,7 +1,7 @@
module.exports = {
tabWidth: 2,
useTabs: false,
endOfLine: 'lf',
endOfLine: 'crlf',
semi: false,
singleQuote: true,
printWidth: 100,
+316 -39
View File
@@ -8,47 +8,247 @@
### 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
<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 |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| 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` |
| 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` |
| SITE_ID | Unique ID of the channel used in the source. | `bbc1` |
| CHANNEL_NAME | Name of the channel used in the source. | `BBC 1` |
| 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. | `hbo` |
| 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?
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>
<summary>example.com.config.js</summary>
<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
module.exports = {
site: 'example.com',
url: function ({ channel, date }) {
url({ channel, date }) {
return `https://example.com/api/${channel.site_id}/${date.format('YYYY-MM-DD')}`
},
parser: function ({ content }) {
return JSON.parse(content)
parser(context) {
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>
@@ -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.
```js
const { url, parser } = require('./example.com.config.js')
const { parser, url } = require('./example.com.config.js')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const date = dayjs.utc('2022-11-18', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'bbc1', xmltv_id: 'BBCOne.uk', lang: 'en' }
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'bbc1', xmltv_id: 'BBCOne.uk' }
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', () => {
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 })
expect(results).toMatchObject([
{
start: '2022-11-18T01:30:00.000Z',
stop: '2022-11-18T02:00:00.000Z',
title: 'Program 1'
}
])
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: '' })
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
</details>
@@ -110,21 +316,89 @@ This file contains a list of channels available at the source.
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<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>
```
</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
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
@@ -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.
- `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).
- `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.
- `channels:lint`: сhecks the channel lists for syntax errors.
- `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.
- `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.
- `serve`: starts the [web server](https://github.com/vercel/serve).
- `lint`: сhecks the scripts for syntax errors.
+24
View File
@@ -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" ]
+98 -35
View File
@@ -1,4 +1,4 @@
# EPG
# EPG [![update](https://github.com/iptv-org/epg/actions/workflows/update.yml/badge.svg)](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.
@@ -7,6 +7,7 @@ Tools for downloading the EPG (Electronic Program Guide) for thousands of TV cha
- ✨ [Installation](#installation)
- 🚀 [Usage](#usage)
- 💫 [Update](#update)
- 🐋 [Docker](#docker)
- 📺 [Playlists](#playlists)
- 🗄 [Database](#database)
- 👨‍💻 [API](#api)
@@ -39,13 +40,15 @@ npm install
## 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
```
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.
You can also customize the behavior of the script using this options:
```sh
Usage: npm run grab --- [options]
@@ -55,34 +58,14 @@ Options:
-c, --channels <path> Path to *.channels.xml file (required if the "--site" attribute is
not specified)
-o, --output <path> Path to output file (default: "guide.xml")
-l, --lang <code> Filter channels by language (ISO 639-2 code)
-t, --timeout <milliseconds> Override the default timeout for each request
-d, --delay <milliseconds> Override the default delay between request
--days <days> Override the number of days for which the program will be loaded
(defaults to the value from the site config)
--maxConnections <number> Limit on the number of concurrent requests (default: 1)
--cron <expression> Schedule a script run (example: "0 0 * * *")
--gzip Create a compressed version of the guide as well (default: false)
```
### Access the guide by URL
You can make the guide available via URL by running your own server:
```sh
npm run serve
```
After that, the guide will be available at the link:
```
http://localhost:3000/guide.xml
```
In addition it will be available to other devices on the same local network at the address:
```
http://<your_local_ip_address>:3000/guide.xml
-l, --lang <codes> Allows you to restrict downloading to channels in specified languages only (example: "en,id")
-t, --timeout <milliseconds> Timeout for each request in milliseconds (default: 0)
-d, --delay <milliseconds> Delay between request in milliseconds (default: 0)
-x, --proxy <url> Use the specified proxy (example: "socks5://username:password@127.0.0.1:1234")
--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)
--curl Display each request as CURL (default: false)
```
### Parallel downloading
@@ -115,12 +98,38 @@ npm run grab --- --channels=path/to/custom.channels.xml
### Run on schedule
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:
If you want to download guides on a schedule, you can use [cron](https://en.wikipedia.org/wiki/Cron) or any other task scheduler. Currently, we use a tool called `chronos` for this purpose.
To start it, you only need to specify the necessary `grab` command and [cron expression](https://crontab.guru/):
```sh
npm run grab --- --site=example.com --cron="0 0 * * *"
npx chronos --execute="npm run grab --- --site=example.com" --pattern="0 0,12 * * *" --log
```
For more info go to [chronos](https://github.com/freearhey/chronos) documentation.
### Access the guide by URL
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
npx serve
```
After that, the guide will be available at the link:
```
http://localhost:3000/guide.xml
```
In addition it will be available to other devices on the same local network at the address:
```
http://<your_local_ip_address>:3000/guide.xml
```
For more info go to [serve](https://github.com/vercel/serve) documentation.
## Update
If you have downloaded the repository code according to the instructions above, then to update it will be enough to run the command:
@@ -135,9 +144,63 @@ And then update all the dependencies:
npm install
```
## Playlists
## Docker
Playlists with already linked guides can be found in the [iptv-org/iptv](https://github.com/iptv-org/iptv) repository.
### Build an image
```sh
docker build -t iptv-org/epg --no-cache .
```
### Create and run container
```sh
docker run -p 3000:3000 -v /path/to/channels.xml:/epg/channels.xml iptv-org/epg
```
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.
From the outside, it will be available at this link:
```
http://localhost:3000/guide.xml
```
or
```
http://<your_local_ip_address>:3000/guide.xml
```
### 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
+240 -201
View File
@@ -1,203 +1,242 @@
# Sites
| Site | Status | Notes |
| ------------------------------------------------------------------ | ------ | ---------------------------------------------------------------------------------------- |
| [9tv.co.il](sites/9tv.co.il) | 🟢 | |
| [abc.net.au](sites/abc.net.au) | 🟢 | |
| [allente.dk](sites/allente.dk) | 🟢 | |
| [allente.fi](sites/allente.fi) | 🟢 | |
| [allente.no](sites/allente.no) | 🟢 | |
| [allente.se](sites/allente.se) | 🟢 | |
| [andorradifusio.ad](sites/andorradifusio.ad) | 🟢 | |
| [anteltv.com.uy](sites/anteltv.com.uy) | 🟢 | |
| [arianaafgtv.com](sites/arianaafgtv.com) | 🟢 | |
| [arianatelevision.com](sites/arianatelevision.com) | 🟢 | |
| [arirang.com](sites/arirang.com) | 🟢 | |
| [artonline.tv](sites/artonline.tv) | 🟢 | |
| [awilime.com](sites/awilime.com) | 🟢 | |
| [bein.com](sites/bein.com) | 🟢 | |
| [beinsports.com](sites/beinsports.com) | 🟢 | |
| [berrymedia.co.kr](sites/berrymedia.co.kr) | 🟢 | |
| [cablego.com.pe](sites/cablego.com.pe) | 🟢 | |
| [cableplus.com.uy](sites/cableplus.com.uy) | 🟢 | |
| [canalplus-haiti.com](sites/canalplus-haiti.com) | 🔴 | https://github.com/iptv-org/epg/issues/2237 |
| [canalplus.com](sites/canalplus.com) | 🟢 | |
| [cgates.lt](sites/cgates.lt) | 🟢 | |
| [chada.ma](sites/chada.ma) | 🟢 | |
| [chaines-tv.orange.fr](sites/chaines-tv.orange.fr) | 🟡 | https://github.com/iptv-org/epg/issues/2395 |
| [clickthecity.com](sites/clickthecity.com) | 🟢 | |
| [comteco.com.bo](sites/comteco.com.bo) | 🔴 | https://github.com/iptv-org/epg/issues/2239 |
| [content.astro.com.my](sites/content.astro.com.my) | 🟢 | |
| [cosmote.gr](sites/cosmote.gr) | 🟢 | |
| [cubmu.com](sites/cubmu.com) | 🟢 | |
| [dens.tv](sites/dens.tv) | 🟢 | |
| [digiturk.com.tr](sites/digiturk.com.tr) | 🔴 | https://github.com/iptv-org/epg/issues/2304, https://github.com/iptv-org/epg/issues/2547 |
| [directv.com](sites/directv.com) | 🔴 | https://github.com/iptv-org/epg/issues/2284 |
| [directv.com.ar](sites/directv.com.ar) | 🔴 | https://github.com/iptv-org/epg/issues/2248 |
| [directv.com.uy](sites/directv.com.uy) | 🟢 | |
| [dishtv.in](sites/dishtv.in) | 🔴 | https://github.com/iptv-org/epg/issues/2445 |
| [dsmart.com.tr](sites/dsmart.com.tr) | 🟡 | https://github.com/iptv-org/epg/issues/2303 |
| [dstv.com](sites/dstv.com) | 🟢 | |
| [elcinema.com](sites/elcinema.com) | 🔴 | https://github.com/iptv-org/epg/issues/2541 |
| [ena.skylifetv.co.kr](sites/ena.skylifetv.co.kr) | 🟢 | |
| [energeek.cl](sites/energeek.cl) | 🟢 | |
| [entertainment.ie](sites/entertainment.ie) | 🟢 | |
| [firstmedia.com](sites/firstmedia.com) | 🟢 | |
| [flixed.io](sites/flixed.io) | 🟡 | https://github.com/iptv-org/epg/issues/2543 |
| [foxsports.com.au](sites/foxsports.com.au) | 🟢 | |
| [foxtel.com.au](sites/foxtel.com.au) | 🟢 | |
| [frikanalen.no](sites/frikanalen.no) | 🟢 | |
| [gatotv.com](sites/gatotv.com) | 🟢 | |
| [getafteritmedia.com](sites/getafteritmedia.com) | 🟢 | |
| [guida.tv](sites/guida.tv) | 🟢 | |
| [guidatv.sky.it](sites/guidatv.sky.it) | 🟢 | |
| [hd-plus.de](sites/hd-plus.de) | 🔴 | https://github.com/iptv-org/epg/issues/2173 |
| [horizon.tv](sites/horizon.tv) | 🟢 | |
| [hoy.tv](sites/hoy.tv) | 🟢 | |
| [i.mjh.nz](sites/i.mjh.nz) | 🟢 | |
| [i24news.tv](sites/i24news.tv) | 🟢 | |
| [iltalehti.fi](sites/iltalehti.fi) | 🟡 | https://github.com/iptv-org/epg/issues/2396 |
| [indihometv.com](sites/indihometv.com) | 🟢 | |
| [ionplustv.com](sites/ionplustv.com) | 🟢 | |
| [ipko.tv](sites/ipko.tv) | 🟢 | |
| [kan.org.il](sites/kan.org.il) | 🔴 | https://github.com/iptv-org/epg/issues/2273 |
| [knr.gl](sites/knr.gl) | 🟢 | |
| [kplus.vn](sites/kplus.vn) | 🔴 | https://github.com/iptv-org/epg/issues/2240 |
| [kvf.fo](sites/kvf.fo) | 🟢 | |
| [m.tv.sms.cz](sites/m.tv.sms.cz) | 🔴 | https://github.com/iptv-org/epg/issues/2241 |
| [m.tving.com](sites/m.tving.com) | 🟢 | |
| [magticom.ge](sites/magticom.ge) | 🟢 | |
| [mako.co.il](sites/mako.co.il) | 🟢 | |
| [maxtv.hrvatskitelekom.hr](sites/maxtv.hrvatskitelekom.hr) | 🔴 | https://github.com/iptv-org/epg/issues/2509 |
| [maxtvgo.mk](sites/maxtvgo.mk) | 🟢 | |
| [mediagenie.co.kr](sites/mediagenie.co.kr) | 🟢 | |
| [mediaklikk.hu](sites/mediaklikk.hu) | 🟢 | |
| [mediasetinfinity.mediaset.it](sites/mediasetinfinity.mediaset.it) | 🟢 | |
| [melita.com](sites/melita.com) | 🟢 | |
| [meo.pt](sites/meo.pt) | 🟡 | https://github.com/iptv-org/epg/issues/2446 |
| [meuguia.tv](sites/meuguia.tv) | 🟢 | |
| [mewatch.sg](sites/mewatch.sg) | 🟢 | |
| [mi.tv](sites/mi.tv) | 🟢 | |
| [mncvision.id](sites/mncvision.id) | 🟢 | |
| [moji.id](sites/moji.id) | 🟢 | |
| [mon-programme-tv.be](sites/mon-programme-tv.be) | 🟢 | |
| [movistarplus.es](sites/movistarplus.es) | 🔴 | https://github.com/iptv-org/epg/issues/2498 |
| [mtel.ba](sites/mtel.ba) | 🟢 | |
| [mts.rs](sites/mts.rs) | 🟢 | |
| [mujtvprogram.cz](sites/mujtvprogram.cz) | 🟢 | |
| [musor.tv](sites/musor.tv) | 🟢 | |
| [mysky.com.ph](sites/mysky.com.ph) | 🟢 | |
| [mytelly.co.uk](sites/mytelly.co.uk) | 🟢 | |
| [mytvsuper.com](sites/mytvsuper.com) | 🟢 | |
| [neo.io](sites/neo.io) | 🟢 | |
| [nhkworldpremium.com](sites/nhkworldpremium.com) | 🟢 | |
| [nhl.com](sites/nhl.com) | 🟢 | |
| [nostv.pt](sites/nostv.pt) | 🟢 | |
| [novacyprus.com](sites/novacyprus.com) | 🟢 | |
| [novasports.gr](sites/novasports.gr) | 🟢 | |
| [nowplayer.now.com](sites/nowplayer.now.com) | 🟢 | |
| [nuevosiglo.com.uy](sites/nuevosiglo.com.uy) | 🟢 | |
| [nzxmltv.com](sites/nzxmltv.com) | 🟢 | |
| [ontvtonight.com](sites/ontvtonight.com) | 🟢 | |
| [osn.com](sites/osn.com) | 🟢 | |
| [pbsguam.org](sites/pbsguam.org) | 🟢 | |
| [pickx.be](sites/pickx.be) | 🟢 | |
| [player.ee.co.uk](sites/player.ee.co.uk) | 🟢 | |
| [playtv.unifi.com.my](sites/playtv.unifi.com.my) | 🟢 | |
| [plex.tv](sites/plex.tv) | 🟢 | |
| [programacion-tv.elpais.com](sites/programacion-tv.elpais.com) | 🟢 | |
| [programacion.tcc.com.uy](sites/programacion.tcc.com.uy) | 🟢 | |
| [programetv.ro](sites/programetv.ro) | 🟢 | |
| [programme-tv.net](sites/programme-tv.net) | 🟢 | |
| [programme-tv.vini.pf](sites/programme-tv.vini.pf) | 🟢 | |
| [programme.tvb.com](sites/programme.tvb.com) | 🟢 | |
| [programtv.onet.pl](sites/programtv.onet.pl) | 🟢 | |
| [raiplay.it](sites/raiplay.it) | 🟢 | |
| [reportv.com.ar](sites/reportv.com.ar) | 🟢 | |
| [rev.bs](sites/rev.bs) | 🔴 | https://github.com/iptv-org/epg/issues/2255 |
| [rotana.net](sites/rotana.net) | 🟢 | |
| [rtb.gov.bn](sites/rtb.gov.bn) | 🔴 | https://github.com/iptv-org/epg/issues/2257 |
| [rthk.hk](sites/rthk.hk) | 🟢 | |
| [rtmklik.rtm.gov.my](sites/rtmklik.rtm.gov.my) | 🟢 | |
| [rtp.pt](sites/rtp.pt) | 🟢 | |
| [ruv.is](sites/ruv.is) | 🟢 | |
| [s.mxtv.jp](sites/s.mxtv.jp) | 🟢 | |
| [sat.tv](sites/sat.tv) | 🟢 | |
| [shahid.mbc.net](sites/shahid.mbc.net) | 🟢 | |
| [siba.com.co](sites/siba.com.co) | 🟢 | |
| [singtel.com](sites/singtel.com) | 🟢 | |
| [sjonvarp.is](sites/sjonvarp.is) | 🟢 | |
| [sky.co.nz](sites/sky.co.nz) | 🟢 | |
| [sky.com](sites/sky.com) | 🟡 | https://github.com/iptv-org/epg/issues/2501, https://github.com/iptv-org/epg/issues/2516 |
| [sky.de](sites/sky.de) | 🟢 | |
| [skylife.co.kr](sites/skylife.co.kr) | 🟢 | |
| [skyperfecttv.co.jp](sites/skyperfecttv.co.jp) | 🟢 | |
| [sporttv.pt](sites/sporttv.pt) | 🟢 | |
| [starhubtvplus.com](sites/starhubtvplus.com) | 🟢 | |
| [startimestv.com](sites/startimestv.com) | 🟢 | |
| [streamingtvguides.com](sites/streamingtvguides.com) | 🟢 | |
| [superguidatv.it](sites/superguidatv.it) | 🟢 | |
| [taiwanplus.com](sites/taiwanplus.com) | 🟢 | |
| [tapdmv.com](sites/tapdmv.com) | 🟢 | |
| [telenet.tv](sites/telenet.tv) | 🟢 | |
| [teliatv.ee](sites/teliatv.ee) | 🟢 | |
| [telkussa.fi](sites/telkussa.fi) | 🟢 | |
| [telsu.fi](sites/telsu.fi) | 🟢 | |
| [tivu.tv](sites/tivu.tv) | 🟢 | |
| [toonamiaftermath.com](sites/toonamiaftermath.com) | 🟢 | |
| [turksatkablo.com.tr](sites/turksatkablo.com.tr) | 🟢 | |
| [tv-programme.telecablesat.fr](sites/tv-programme.telecablesat.fr) | 🟢 | |
| [tv.blue.ch](sites/tv.blue.ch) | 🟢 | |
| [tv.cctv.com](sites/tv.cctv.com) | 🟢 | |
| [tv.dir.bg](sites/tv.dir.bg) | 🟢 | |
| [tv.lv](sites/tv.lv) | 🟢 | |
| [tv.magenta.at](sites/tv.magenta.at) | 🟢 | |
| [tv.mail.ru](sites/tv.mail.ru) | 🟢 | |
| [tv.movistar.com.pe](sites/tv.movistar.com.pe) | 🟢 | |
| [tv.nu](sites/tv.nu) | 🟢 | |
| [tv.post.lu](sites/tv.post.lu) | 🟢 | |
| [tv.trueid.net](sites/tv.trueid.net) | 🟢 | |
| [tv.yandex.ru](sites/tv.yandex.ru) | 🟢 | |
| [tv.yettel.hu](sites/tv.yettel.hu) | 🔴 | https://github.com/iptv-org/epg/issues/2263 |
| [tv24.co.uk](sites/tv24.co.uk) | 🟢 | |
| [tv24.se](sites/tv24.se) | 🟢 | |
| [tv2go.t-2.net](sites/tv2go.t-2.net) | 🟢 | |
| [tva.tv](sites/tva.tv) | 🔴 | https://github.com/iptv-org/epg/issues/2264 |
| [tvarenasport.com](sites/tvarenasport.com) | 🟢 | |
| [tvarenasport.hr](sites/tvarenasport.hr) | 🟢 | |
| [tvcesoir.fr](sites/tvcesoir.fr) | 🟢 | |
| [tvcubana.icrt.cu](sites/tvcubana.icrt.cu) | 🟢 | |
| [tvgids.nl](sites/tvgids.nl) | 🟡 | https://github.com/iptv-org/epg/issues/2400 |
| [tvguide.com](sites/tvguide.com) | 🔴 | https://github.com/iptv-org/epg/issues/2542 |
| [tvguide.myjcom.jp](sites/tvguide.myjcom.jp) | 🟢 | |
| [tvhebdo.com](sites/tvhebdo.com) | 🟢 | |
| [tvheute.at](sites/tvheute.at) | 🟢 | |
| [tvim.tv](sites/tvim.tv) | 🟢 | |
| [tvireland.ie](sites/tvireland.ie) | 🟢 | |
| [tvmi.mt](sites/tvmi.mt) | 🟢 | |
| [tvmusor.hu](sites/tvmusor.hu) | 🟢 | |
| [tvpassport.com](sites/tvpassport.com) | 🟢 | |
| [tvplus.com.tr](sites/tvplus.com.tr) | 🟢 | |
| [tvprofil.com](sites/tvprofil.com) | 🟡 | https://github.com/iptv-org/epg/issues/2399 |
| [tvtv.us](sites/tvtv.us) | 🟢 | |
| [v3.myafn.dodmedia.osd.mil](sites/v3.myafn.dodmedia.osd.mil) | 🟢 | |
| [vidio.com](sites/vidio.com) | 🟢 | |
| [virginmediatelevision.ie](sites/virginmediatelevision.ie) | 🟢 | |
| [virgintvgo.virginmedia.com](sites/virgintvgo.virginmedia.com) | 🟢 | |
| [visionplus.id](sites/visionplus.id) | 🟢 | |
| [vivacom.bg](sites/vivacom.bg) | 🔴 | https://github.com/iptv-org/epg/issues/2270 |
| [vtm.be](sites/vtm.be) | 🟢 | |
| [walesi.com.fj](sites/walesi.com.fj) | 🟢 | |
| [watch.sportsnet.ca](sites/watch.sportsnet.ca) | 🟢 | |
| [watchyour.tv](sites/watchyour.tv) | 🟢 | |
| [wavve.com](sites/wavve.com) | 🟢 | |
| [web.magentatv.de](sites/web.magentatv.de) | 🟢 | |
| [webtv.delta.nl](sites/webtv.delta.nl) | 🟢 | |
| [winplay.co](sites/winplay.co) | 🟢 | |
| [worldfishingnetwork.com](sites/worldfishingnetwork.com) | 🟢 | |
| [www3.nhk.or.jp](sites/www3.nhk.or.jp) | 🟢 | |
| [xumo.tv](sites/xumo.tv) | 🟢 | |
| [zap.co.ao](sites/zap.co.ao) | 🟢 | |
| [ziggogo.tv](sites/ziggogo.tv) | 🟡 | https://github.com/iptv-org/epg/issues/2393 |
| [znbc.co.zm](sites/znbc.co.zm) | 🟢 | |
| [zuragt.mn](sites/zuragt.mn) | 🟢 | |
<table>
<thead>
<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>
</thead>
<tbody>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
+57
View File
@@ -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__/']
}
]
+10050 -6641
View File
File diff suppressed because it is too large Load Diff
+96 -64
View File
@@ -1,91 +1,123 @@
{
"name": "epg",
"scripts": {
"act:check": "act pull_request -W .github/workflows/check.yml",
"act:update": "act workflow_dispatch -W .github/workflows/update.yml",
"api:load": "npx tsx scripts/commands/api/load.ts",
"api:generate": "npx tsx scripts/commands/api/generate.ts",
"channels:lint": "npx tsx scripts/commands/channels/lint.ts",
"channels:parse": "npx tsx scripts/commands/channels/parse.ts",
"channels:editor": "npx tsx scripts/commands/channels/editor.ts",
"channels:validate": "npx tsx scripts/commands/channels/validate.ts",
"grab": "npx tsx scripts/commands/epg/grab.ts",
"serve": "npx serve",
"lint": "npx eslint \"{scripts,tests}/**/*.{ts,js}\"",
"test": "run-script-os",
"test:win32": "SET \"TZ=Pacific/Nauru\" && npx jest --runInBand",
"test:default": "TZ=Pacific/Nauru npx jest --runInBand",
"postinstall": "npm run api:load"
"api:load": "tsx scripts/commands/api/load.ts",
"api:generate": "tsx scripts/commands/api/generate.ts",
"channels:lint": "tsx scripts/commands/channels/lint.mts",
"channels:parse": "tsx scripts/commands/channels/parse.ts",
"channels:edit": "tsx scripts/commands/channels/edit.ts",
"channels:validate": "tsx scripts/commands/channels/validate.ts",
"sites:init": "tsx scripts/commands/sites/init.ts",
"sites:update": "tsx scripts/commands/sites/update.ts",
"grab": "tsx scripts/commands/epg/grab.ts",
"lint": "npx eslint \"{scripts,tests,sites}/**/*.{ts,mts,js}\"",
"test": "cross-env TZ=Pacific/Nauru npx jest --runInBand",
"postinstall": "skip-postinstall || npm run api:load",
"prepare": "husky"
},
"private": true,
"author": "Arhey",
"license": "UNLICENSED",
"jest": {
"setupFiles": [
"<rootDir>/node_modules/jest-offline"
],
"transform": {
"^.+\\.(ts|js)$": "ts-jest"
"^.+\\.(ts|js)$": "@swc/jest"
},
"testRegex": "(tests|sites)/(.*?/)?.*test.(js|ts)$",
"testTimeout": 10000
"testMatch": [
"<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": {
"@alex_neo/jest-expect-message": "^1.0.5",
"@freearhey/core": "^0.3.1",
"@freearhey/search-js": "^0.1.1",
"@ntlab/sfetch": "^1.0.0",
"@octokit/core": "^4.1.0",
"@types/cli-progress": "^3.11.3",
"@types/fs-extra": "^11.0.2",
"@types/inquirer": "^9.0.3",
"@types/jest": "^29.5.5",
"@types/lodash": "^4.14.199",
"@types/node-cleanup": "^2.1.2",
"@types/numeral": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"axios": "^1.5.1",
"axios-cookiejar-support": "^5.0.4",
"chalk": "^4.1.2",
"cheerio": "^1.0.0-rc.10",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.32.0",
"@freearhey/chronos": "^0.0.1",
"@freearhey/core": "^0.10.2",
"@freearhey/search-js": "^0.1.2",
"@ntlab/sfetch": "^1.2.0",
"@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-rest": "^13.1.1",
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
"@stylistic/eslint-plugin": "^5.2.2",
"@swc/core": "^1.13.2",
"@swc/jest": "^0.2.39",
"@types/cli-progress": "^3.11.6",
"@types/fs-extra": "^11.0.4",
"@types/inquirer": "^9.0.8",
"@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",
"commander": "^8.2.0",
"consola": "^3.2.3",
"cron": "^2.4.3",
"csv-parser": "^3.0.0",
"commander": "^14.0.0",
"consola": "^3.4.2",
"cross-env": "^10.0.0",
"csv-parser": "^3.2.0",
"cwait": "^1.1.2",
"dayjs": "^1.11.10",
"epg-grabber": "^0.37.4",
"dayjs": "^1.11.13",
"epg-grabber": "^0.41.0",
"epg-parser": "^0.3.1",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.0.0",
"form-data": "^4.0.0",
"fs-extra": "^10.0.1",
"glob": "^7.2.0",
"iconv-lite": "^0.4.24",
"inquirer": "^8.2.6",
"jest": "^29.7.0",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"form-data": "^4.0.4",
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"globals": "^16.3.0",
"husky": "^9.1.7",
"iconv-lite": "^0.6.3",
"inquirer": "^12.8.2",
"jest": "^30.0.5",
"jest-offline": "^1.0.1",
"langs": "^2.0.0",
"libxmljs2": "^0.35.0",
"lodash": "^4.17.21",
"luxon": "^3.3.0",
"markdown-include": "^0.4.3",
"libxml2-wasm": "^0.5.0",
"lodash.orderby": "^4.6.0",
"lodash.sortby": "^4.7.0",
"lodash.startcase": "^4.4.0",
"lodash.uniqby": "^4.7.0",
"luxon": "^3.7.1",
"mockdate": "^3.0.5",
"nedb-promises": "^6.0.3",
"nedb-promises": "^6.2.3",
"node-cleanup": "^2.1.2",
"node-gzip": "^1.1.2",
"numeral": "^2.0.6",
"parse-duration": "^1.0.0",
"pako": "^2.1.0",
"parse-duration": "^2.1.4",
"pdf-parse": "^1.1.1",
"pm2": "^6.0.8",
"readline": "^1.3.0",
"run-script-os": "^1.1.6",
"serve": "^14.2.4",
"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",
"tabletojson": "^2.0.7",
"tough-cookie": "^5.0.0",
"transliteration": "^2.2.0",
"ts-jest": "^29.1.1",
"tsx": "^4.19.2",
"unzipit": "^1.4.0",
"wildcard-match": "^5.1.2"
},
"devDependencies": {
"run-script-os": "^1.1.6"
"tabletojson": "^4.1.6",
"tough-cookie": "^5.1.2",
"transliteration": "^2.3.5",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"unzipit": "^1.4.3",
"uuid": "^11.1.0",
"wildcard-match": "^5.1.4"
}
}
+24
View File
@@ -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
}
]
}
+22 -32
View File
@@ -1,16 +1,9 @@
import { Logger, Storage, Collection } from '@freearhey/core'
import { ChannelsParser } from '../../core'
import path from 'path'
import { Logger, Collection, Storage } from '@freearhey/core'
import { SITES_DIR, API_DIR } from '../../constants'
import { Channel } from 'epg-grabber'
type OutputItem = {
channel: string | null
site: string
site_id: string
site_name: string
lang: string
}
import { GuideChannel } from '../../models'
import { ChannelsParser } from '../../core'
import epgGrabber from 'epg-grabber'
import path from 'path'
async function main() {
const logger = new Logger()
@@ -19,28 +12,25 @@ async function main() {
logger.info('loading channels...')
const sitesStorage = new Storage(SITES_DIR)
const parser = new ChannelsParser({ storage: sitesStorage })
let files: string[] = []
files = await sitesStorage.list('**/*.channels.xml')
let parsedChannels = new Collection()
for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath))
}
logger.info(` found ${parsedChannels.count()} channel(s)`)
const output = parsedChannels.map((channel: Channel): OutputItem => {
return {
channel: channel.xmltv_id || null,
site: channel.site || '',
site_id: channel.site_id || '',
site_name: channel.name,
lang: channel.lang || ''
}
const parser = new ChannelsParser({
storage: sitesStorage
})
const files: string[] = await sitesStorage.list('**/*.channels.xml')
const channels = new Collection()
for (const filepath of files) {
const channelList = await parser.parse(filepath)
channelList.channels.forEach((data: epgGrabber.Channel) => {
channels.add(new GuideChannel(data))
})
}
logger.info(`found ${channels.count()} channel(s)`)
const output = channels.map((channel: GuideChannel) => channel.toJSON())
const apiStorage = new Storage(API_DIR)
const outputFilename = 'guides.json'
await apiStorage.save('guides.json', output.toJSON())
+19 -12
View File
@@ -1,18 +1,25 @@
import { Logger } from '@freearhey/core'
import { ApiClient } from '../../core'
import { DATA_DIR } from '../../constants'
import { Storage } from '@freearhey/core'
import { DataLoader } from '../../core'
async function main() {
const logger = new Logger()
const client = new ApiClient({ logger })
const storage = new Storage(DATA_DIR)
const loader = new DataLoader({ storage })
const requests = [
client.download('channels.json'),
client.download('countries.json'),
client.download('regions.json'),
client.download('subdivisions.json')
]
await Promise.all(requests)
await Promise.all([
loader.download('blocklist.json'),
loader.download('categories.json'),
loader.download('channels.json'),
loader.download('countries.json'),
loader.download('languages.json'),
loader.download('regions.json'),
loader.download('subdivisions.json'),
loader.download('feeds.json'),
loader.download('timezones.json'),
loader.download('guides.json'),
loader.download('streams.json'),
loader.download('logos.json')
])
}
main()
-1
View File
@@ -1 +0,0 @@
/replace.ts
+216
View File
@@ -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, '\\$&')
}
-179
View File
@@ -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}`
}
+109
View File
@@ -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()
-78
View File
@@ -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()
+35 -42
View File
@@ -1,8 +1,9 @@
import { Logger, File, Collection, Storage } from '@freearhey/core'
import { ChannelsParser, XML } from '../../core'
import { Channel } from 'epg-grabber'
import { Logger, File, Storage } from '@freearhey/core'
import { ChannelsParser } from '../../core'
import { ChannelList } from '../../models'
import { pathToFileURL } from 'node:url'
import epgGrabber from 'epg-grabber'
import { Command } from 'commander'
import path from 'path'
const program = new Command()
program
@@ -11,7 +12,7 @@ program
.option('-o, --output <output>', 'Output file')
.parse(process.argv)
type ParseOptions = {
interface ParseOptions {
config: string
set?: string
output?: string
@@ -21,22 +22,28 @@ type ParseOptions = {
const options: ParseOptions = program.opts()
async function main() {
const storage = new Storage()
const parser = new ChannelsParser({ storage })
const logger = new Logger()
const file = new File(options.config)
const dir = file.dirname()
const config = require(path.resolve(options.config))
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
let channels = new Collection()
if (await storage.exists(outputFilepath)) {
channels = await parser.parse(outputFilepath)
function isPromise(promise: object[] | Promise<object[]>) {
return (
!!promise &&
typeof promise === 'object' &&
typeof (promise as Promise<object[]>).then === 'function'
)
}
const args: {
[key: string]: string
} = {}
const storage = new Storage()
const logger = new Logger()
const parser = new ChannelsParser({ storage })
const file = new File(options.config)
const dir = file.dirname()
const config = (await import(pathToFileURL(options.config).toString())).default
const outputFilepath = options.output || `${dir}/${config.site}.channels.xml`
let channelList = new ChannelList({ channels: [] })
if (await storage.exists(outputFilepath)) {
channelList = await parser.parse(outputFilepath)
}
const args: Record<string, string> = {}
if (Array.isArray(options.set)) {
options.set.forEach((arg: string) => {
@@ -49,45 +56,31 @@ async function main() {
if (isPromise(parsedChannels)) {
parsedChannels = await parsedChannels
}
parsedChannels = parsedChannels.map((channel: Channel) => {
parsedChannels = parsedChannels.map((channel: epgGrabber.Channel) => {
channel.site = config.site
return channel
})
let output = new Collection()
parsedChannels.forEach((channel: Channel) => {
const found: Channel | undefined = channels.first(
(_channel: Channel) => _channel.site_id == channel.site_id
)
const newChannelList = new ChannelList({ channels: [] })
parsedChannels.forEach((channel: epgGrabber.Channel) => {
if (!channel.site_id) return
const found: epgGrabber.Channel | undefined = channelList.get(channel.site_id)
if (found) {
channel.xmltv_id = found.xmltv_id
channel.lang = found.lang
}
output.add(channel)
newChannelList.add(channel)
})
output = output.orderBy([
(channel: Channel) => channel.lang || '_',
(channel: Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
(channel: Channel) => channel.site_id
])
newChannelList.sort()
const xml = new XML(output)
await storage.save(outputFilepath, xml.toString())
await storage.save(outputFilepath, newChannelList.toString())
logger.info(`File '${outputFilepath}' successfully saved`)
}
main()
function isPromise(promise: object[] | Promise<object[]>) {
return (
!!promise &&
typeof promise === 'object' &&
typeof (promise as Promise<object[]>).then === 'function'
)
}
+45 -47
View File
@@ -1,23 +1,18 @@
import { Storage, Collection, Dictionary, File, Logger } from '@freearhey/core'
import { ChannelsParser, ApiChannel } from '../../core'
import { ChannelsParser, DataLoader, DataProcessor } from '../../core'
import { DataProcessorData } from '../../types/dataProcessor'
import { Storage, Dictionary, File } from '@freearhey/core'
import { DataLoaderData } from '../../types/dataLoader'
import { ChannelList } from '../../models'
import { DATA_DIR } from '../../constants'
import epgGrabber from 'epg-grabber'
import { program } from 'commander'
import chalk from 'chalk'
import langs from 'langs'
import { DATA_DIR } from '../../constants'
import { Channel } from 'epg-grabber'
program
.option(
'-c, --channels <path>',
'Path to channels.xml file to validate',
'sites/**/*.channels.xml'
)
.parse(process.argv)
program.argument('[filepath...]', 'Path to *.channels.xml files to validate').parse(process.argv)
const options = program.opts()
type ValidationError = {
type: 'duplicate' | 'wrong_xmltv_id' | 'wrong_lang'
interface ValidationError {
type: 'duplicate' | 'wrong_channel_id' | 'wrong_feed_id' | 'wrong_lang'
name: string
lang?: string
xmltv_id?: string
@@ -26,63 +21,59 @@ type ValidationError = {
}
async function main() {
const logger = new Logger()
logger.info('options:')
logger.tree(options)
const parser = new ChannelsParser({ storage: new Storage() })
const processor = new DataProcessor()
const dataStorage = new Storage(DATA_DIR)
const channelsContent = await dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
const loader = new DataLoader({ storage: dataStorage })
const data: DataLoaderData = await loader.load()
const { channelsKeyById, feedsKeyByStreamId }: DataProcessorData = processor.process(data)
const parser = new ChannelsParser({
storage: new Storage()
})
let totalFiles = 0
let totalErrors = 0
let totalWarnings = 0
const storage = new Storage()
const files: string[] = await storage.list(options.channels)
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 parsedChannels = await parser.parse(filepath)
const channelList: ChannelList = await parser.parse(filepath)
const bufferById = new Dictionary()
const bufferBySiteId = new Dictionary()
const errors: ValidationError[] = []
parsedChannels.forEach((channel: Channel) => {
const bufferSiteId: string = `${channel.site_id}:${channel.lang}`
if (bufferBySiteId.missing(bufferSiteId)) {
bufferBySiteId.set(bufferSiteId, true)
channelList.channels.forEach((channel: epgGrabber.Channel) => {
const bufferId: string = channel.site_id
if (bufferBySiteId.missing(bufferId)) {
bufferBySiteId.set(bufferId, true)
} else {
errors.push({ type: 'duplicate', ...channel })
totalErrors++
}
if (!langs.where('1', channel.lang)) {
if (!langs.where('1', channel.lang ?? '')) {
errors.push({ type: 'wrong_lang', ...channel })
totalErrors++
}
if (!channel.xmltv_id) return
const [channelId, feedId] = channel.xmltv_id.split('@')
const foundChannel = channels.first(
(_channel: ApiChannel) => _channel.id === channel.xmltv_id
)
const foundChannel = channelsKeyById.get(channelId)
if (!foundChannel) {
errors.push({ type: 'wrong_xmltv_id', ...channel })
totalErrors++
errors.push({ type: 'wrong_channel_id', ...channel })
totalWarnings++
}
// if (foundChannel && foundChannel.replacedBy) {
// errors.push({ type: 'replaced', ...channel })
// totalErrors++
// }
// if (foundChannel && foundChannel.closed && !foundChannel.replacedBy) {
// errors.push({ type: 'closed', ...channel })
// totalErrors++
// }
if (feedId) {
const foundFeed = feedsKeyByStreamId.get(channel.xmltv_id)
if (!foundFeed) {
errors.push({ type: 'wrong_feed_id', ...channel })
totalWarnings++
}
}
})
if (errors.length) {
@@ -93,10 +84,17 @@ async function main() {
}
}
const totalProblems = totalWarnings + totalErrors
if (totalProblems > 0) {
console.log(
chalk.red(
`${totalProblems} problems (${totalErrors} errors, ${totalWarnings} warnings) in ${totalFiles} file(s)`
)
)
if (totalErrors > 0) {
console.log(chalk.red(`${totalErrors} error(s) in ${totalFiles} file(s)`))
process.exit(1)
}
}
}
main()
+58 -42
View File
@@ -1,47 +1,65 @@
import { Logger, Timer, Storage, Collection } from '@freearhey/core'
import { program } from 'commander'
import { CronJob } from 'cron'
import { QueueCreator, Job, ChannelsParser } from '../../core'
import { Option, program } from 'commander'
import { SITES_DIR } from '../../constants'
import { Channel } from 'epg-grabber'
import path from 'path'
import { SITES_DIR } from '../../constants'
import { ChannelList } from '../../models'
program
.option('-s, --site <name>', 'Name of the site to parse')
.option(
.addOption(new Option('-s, --site <name>', 'Name of the site to parse'))
.addOption(
new Option(
'-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')
.option('-d, --delay <milliseconds>', 'Override the default delay between request')
.option(
)
.addOption(new Option('-o, --output <path>', 'Path to output file').default('guide.xml'))
.addOption(new Option('-l, --lang <codes>', 'Filter channels by languages (ISO 639-1 codes)'))
.addOption(
new Option('-t, --timeout <milliseconds>', 'Override the default timeout for each request').env(
'TIMEOUT'
)
)
.addOption(
new Option('-d, --delay <milliseconds>', 'Override the default delay between request').env(
'DELAY'
)
)
.addOption(new Option('-x, --proxy <url>', 'Use the specified proxy').env('PROXY'))
.addOption(
new Option(
'--days <days>',
'Override the number of days for which the program will be loaded (defaults to the value from the site config)',
value => parseInt(value)
'Override the number of days for which the program will be loaded (defaults to the value from the site config)'
)
.option(
'--maxConnections <number>',
'Limit on the number of concurrent requests',
value => parseInt(value),
1
.argParser(value => parseInt(value))
.env('DAYS')
)
.option('--cron <expression>', 'Schedule a script run (example: "0 0 * * *")')
.option('--gzip', 'Create a compressed version of the guide as well', false)
.parse(process.argv)
.addOption(
new Option('--maxConnections <number>', 'Limit on the number of concurrent requests')
.default(1)
.env('MAX_CONNECTIONS')
)
.addOption(
new Option('--gzip', 'Create a compressed version of the guide as well')
.default(false)
.env('GZIP')
)
.addOption(new Option('--curl', 'Display each request as CURL').default(false).env('CURL'))
.parse()
export type GrabOptions = {
export interface GrabOptions {
site?: string
channels?: string
output: string
gzip: boolean
curl: boolean
maxConnections: number
timeout?: string
delay?: string
lang?: string
days?: number
cron?: string
proxy?: string
}
const options: GrabOptions = program.opts()
@@ -70,37 +88,35 @@ async function main() {
files = await storage.list(options.channels)
}
let parsedChannels = new Collection()
let channels = new Collection()
for (const filepath of files) {
parsedChannels = parsedChannels.concat(await parser.parse(filepath))
}
if (options.lang) {
parsedChannels = parsedChannels.filter((channel: Channel) => channel.lang === options.lang)
}
logger.info(` found ${parsedChannels.count()} channel(s)`)
const channelList: ChannelList = await parser.parse(filepath)
let runIndex = 1
if (options.cron) {
const cronJob = new CronJob(options.cron, async () => {
logger.info(`run #${runIndex}:`)
await runJob({ logger, parsedChannels })
runIndex++
})
cronJob.start()
} else {
logger.info(`run #${runIndex}:`)
runJob({ logger, parsedChannels })
channels = channels.concat(channelList.channels)
}
if (options.lang) {
channels = channels.filter((channel: Channel) => {
if (!options.lang || !channel.lang) return true
return options.lang.includes(channel.lang)
})
}
logger.info(` found ${channels.count()} channel(s)`)
logger.info('run:')
runJob({ logger, channels })
}
main()
async function runJob({ logger, parsedChannels }: { logger: Logger; parsedChannels: Collection }) {
async function runJob({ logger, channels }: { logger: Logger; channels: Collection }) {
const timer = new Timer()
timer.start()
const queueCreator = new QueueCreator({
parsedChannels,
channels,
logger,
options
})
+45
View File
@@ -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()
+76
View File
@@ -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()
+5
View File
@@ -1,4 +1,9 @@
export const ROOT_DIR = process.env.ROOT_DIR || '.'
export const SITES_DIR = process.env.SITES_DIR || './sites'
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
export const 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'
-79
View File
@@ -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
}
}
+7 -50
View File
@@ -1,59 +1,16 @@
import { Logger, Storage } from '@freearhey/core'
import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios'
import cliProgress, { MultiBar } from 'cli-progress'
import numeral from 'numeral'
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
export class ApiClient {
progressBar: MultiBar
client: AxiosInstance
storage: Storage
logger: Logger
instance: AxiosInstance
constructor({ logger }: { logger: Logger }) {
this.logger = logger
this.client = axios.create({
constructor() {
this.instance = axios.create({
baseURL: 'https://iptv-org.github.io/api',
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)
})
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
return this.instance.get(url, options)
}
}
+6 -8
View File
@@ -1,7 +1,8 @@
import { parseChannels } from 'epg-grabber'
import { Storage, Collection } from '@freearhey/core'
import { Storage } from '@freearhey/core'
import { ChannelList } from '../models'
type ChannelsParserProps = {
interface ChannelsParserProps {
storage: Storage
}
@@ -12,13 +13,10 @@ export class ChannelsParser {
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 channels = parseChannels(content)
parsedChannels = parsedChannels.concat(new Collection(channels))
const parsed = parseChannels(content)
return parsedChannels
return new ChannelList({ channels: parsed })
}
}
+2 -3
View File
@@ -1,5 +1,4 @@
import { SiteConfig } from 'epg-grabber'
import _ from 'lodash'
import { pathToFileURL } from 'url'
export class ConfigLoader {
@@ -7,7 +6,7 @@ export class ConfigLoader {
const fileUrl = pathToFileURL(filepath).toString()
const config = (await import(fileUrl)).default
const defaultConfig = {
days: 2,
days: 1,
delay: 0,
output: 'guide.xml',
request: {
@@ -28,6 +27,6 @@ export class ConfigLoader {
channels: undefined
}
return _.merge(defaultConfig, config)
return { ...defaultConfig, ...config } as SiteConfig
}
}
+103
View File
@@ -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)
})
}
}
+55
View File
@@ -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
}
}
}
+4 -3
View File
@@ -1,5 +1,6 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
const date = {}
@@ -10,4 +11,4 @@ date.getUTC = function (d = null) {
return dayjs.utc().startOf('d')
}
module.exports = date
export default date
+27 -2
View File
@@ -1,10 +1,11 @@
import { EPGGrabber, GrabCallbackData, EPGGrabberMock, SiteConfig, Channel } from 'epg-grabber'
import { Logger, Collection } from '@freearhey/core'
import { Queue } from './'
import { Queue, ProxyParser } from './'
import { GrabOptions } from '../commands/epg/grab'
import { TaskQueue, PromisyClass } from 'cwait'
import { SocksProxyAgent } from 'socks-proxy-agent'
type GrabberProps = {
interface GrabberProps {
logger: Logger
queue: Queue
options: GrabOptions
@@ -14,6 +15,7 @@ export class Grabber {
logger: Logger
queue: Queue
options: GrabOptions
grabber: EPGGrabber | EPGGrabberMock
constructor({ logger, queue, options }: GrabberProps) {
this.logger = logger
@@ -23,6 +25,7 @@ export class Grabber {
}
async grab(): Promise<{ channels: Collection; programs: Collection }> {
const proxyParser = new ProxyParser()
const taskQueue = new TaskQueue(Promise as PromisyClass, this.options.maxConnections)
const total = this.queue.size()
@@ -49,6 +52,28 @@ export class Grabber {
config.delay = delay
}
if (this.options.proxy !== undefined) {
const proxy = proxyParser.parse(this.options.proxy)
if (
proxy.protocol &&
['socks', 'socks5', 'socks5h', 'socks4', 'socks4a'].includes(String(proxy.protocol))
) {
const socksProxyAgent = new SocksProxyAgent(this.options.proxy)
config.request = {
...config.request,
...{ httpAgent: socksProxyAgent, httpsAgent: socksProxyAgent }
}
} else {
config.request = { ...config.request, ...{ proxy } }
}
}
if (this.options.curl === true) {
config.curl = true
}
const _programs = await this.grabber.grab(
channel,
date,
-60
View File
@@ -1,60 +0,0 @@
import { Collection, Logger, DateTime, Storage, Zip } from '@freearhey/core'
import { Channel } from 'epg-grabber'
import { XMLTV } from '../core'
import path from 'path'
type GuideProps = {
channels: Collection
programs: Collection
logger: Logger
filepath: string
gzip: boolean
}
export class Guide {
channels: Collection
programs: Collection
logger: Logger
storage: Storage
filepath: string
gzip: boolean
constructor({ channels, programs, logger, filepath, gzip }: GuideProps) {
this.channels = channels
this.programs = programs
this.logger = logger
this.storage = new Storage(path.dirname(filepath))
this.filepath = filepath
this.gzip = gzip || false
}
async save() {
const channels = this.channels.uniqBy(
(channel: Channel) => `${channel.xmltv_id}:${channel.site}`
)
const programs = this.programs
const currDate = new DateTime(process.env.CURR_DATE || new Date().toISOString(), {
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)
}
}
}
+64 -14
View File
@@ -1,9 +1,14 @@
import { Collection, Logger, Storage, StringTemplate } from '@freearhey/core'
import { Collection, Logger, Zip, Storage, StringTemplate } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
import { OptionValues } from 'commander'
import { Channel, Program } from 'epg-grabber'
import { Guide } from '.'
import { Channel, Feed, Guide } from '../models'
import path from 'path'
import { DataLoader, DataProcessor } from '.'
import { DataLoaderData } from '../types/dataLoader'
import { DataProcessorData } from '../types/dataProcessor'
import { DATA_DIR } from '../constants'
type GuideManagerProps = {
interface GuideManagerProps {
options: OptionValues
logger: Logger
channels: Collection
@@ -12,7 +17,6 @@ type GuideManagerProps = {
export class GuideManager {
options: OptionValues
storage: Storage
logger: Logger
channels: Collection
programs: Collection
@@ -22,22 +26,51 @@ export class GuideManager {
this.logger = logger
this.channels = channels
this.programs = programs
this.storage = new Storage()
}
async createGuides() {
const pathTemplate = new StringTemplate(this.options.output)
const processor = new DataProcessor()
const dataStorage = new Storage(DATA_DIR)
const loader = new DataLoader({ storage: dataStorage })
const data: DataLoaderData = await loader.load()
const { feedsKeyByStreamId, channelsKeyById }: DataProcessorData = processor.process(data)
const groupedChannels = this.channels
.orderBy([(channel: Channel) => channel.xmltv_id])
.uniqBy((channel: Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`)
.groupBy((channel: Channel) => {
.map((channel: epgGrabber.Channel) => {
if (channel.xmltv_id && !channel.icon) {
const foundFeed: Feed = feedsKeyByStreamId.get(channel.xmltv_id)
if (foundFeed && foundFeed.hasLogo()) {
channel.icon = foundFeed.getLogoUrl()
} else {
const [channelId] = channel.xmltv_id.split('@')
const foundChannel: Channel = channelsKeyById.get(channelId)
if (foundChannel && foundChannel.hasLogo()) {
channel.icon = foundChannel.getLogoUrl()
}
}
}
return channel
})
.orderBy([
(channel: epgGrabber.Channel) => channel.index,
(channel: epgGrabber.Channel) => channel.xmltv_id
])
.uniqBy(
(channel: epgGrabber.Channel) => `${channel.xmltv_id}:${channel.site}:${channel.lang}`
)
.groupBy((channel: epgGrabber.Channel) => {
return pathTemplate.format({ lang: channel.lang || 'en', site: channel.site || '' })
})
const groupedPrograms = this.programs
.orderBy([(program: Program) => program.channel, (program: Program) => program.start])
.groupBy((program: Program) => {
.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
@@ -51,11 +84,28 @@ export class GuideManager {
filepath: groupKey,
gzip: this.options.gzip,
channels: new Collection(groupedChannels.get(groupKey)),
programs: new Collection(groupedPrograms.get(groupKey)),
logger: this.logger
programs: new Collection(groupedPrograms.get(groupKey))
})
await guide.save()
await this.save(guide)
}
}
async save(guide: Guide) {
const storage = new Storage(path.dirname(guide.filepath))
const xmlFilepath = guide.filepath
const xmlFilename = path.basename(xmlFilepath)
this.logger.info(` saving to "${xmlFilepath}"...`)
const xmltv = guide.toString()
await storage.save(xmlFilename, xmltv)
if (guide.gzip) {
const zip = new Zip()
const compressed = zip.compress(xmltv)
const gzFilepath = `${guide.filepath}.gz`
const gzFilename = path.basename(gzFilepath)
this.logger.info(` saving to "${gzFilepath}"...`)
await storage.save(gzFilename, compressed)
}
}
}
+55
View File
@@ -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
}
}
+12 -10
View File
@@ -1,12 +1,14 @@
export * from './xml'
export * from './channelsParser'
export * from './xmltv'
export * from './configLoader'
export * from './grabber'
export * from './job'
export * from './queue'
export * from './guideManager'
export * from './guide'
export * from './apiChannel'
export * from './apiClient'
export * from './channelsParser'
export * from './configLoader'
export * from './dataLoader'
export * from './dataProcessor'
export * from './grabber'
export * from './guideManager'
export * from './htmlTable'
export * from './issueLoader'
export * from './issueParser'
export * from './job'
export * from './proxyParser'
export * from './queue'
export * from './queueCreator'
+37
View File
@@ -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)
}
}
+34
View File
@@ -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 })
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { Logger } from '@freearhey/core'
import { Queue, Grabber, GuideManager } from '.'
import { GrabOptions } from '../commands/epg/grab'
type JobProps = {
interface JobProps {
options: GrabOptions
logger: Logger
queue: Queue
+31
View File
@@ -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
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { Dictionary } from '@freearhey/core'
import { SiteConfig, Channel } from 'epg-grabber'
export type QueueItem = {
export interface QueueItem {
channel: Channel
date: string
config: SiteConfig
+12 -24
View File
@@ -1,14 +1,14 @@
import { Storage, Collection, DateTime, Logger } from '@freearhey/core'
import { ChannelsParser, ConfigLoader, ApiChannel, Queue } from './'
import { SITES_DIR, DATA_DIR } from '../constants'
import { Channel, SiteConfig } from 'epg-grabber'
import path from 'path'
import { GrabOptions } from '../commands/epg/grab'
import { ConfigLoader, Queue } from './'
import { SiteConfig } from 'epg-grabber'
import path from 'path'
type QueueCreatorProps = {
interface QueueCreatorProps {
logger: Logger
options: GrabOptions
parsedChannels: Collection
channels: Collection
}
export class QueueCreator {
@@ -16,41 +16,29 @@ export class QueueCreator {
logger: Logger
sitesStorage: Storage
dataStorage: Storage
parser: ChannelsParser
parsedChannels: Collection
channels: Collection
options: GrabOptions
constructor({ parsedChannels, logger, options }: QueueCreatorProps) {
this.parsedChannels = parsedChannels
constructor({ channels, logger, options }: QueueCreatorProps) {
this.channels = channels
this.logger = logger
this.sitesStorage = new Storage()
this.dataStorage = new Storage(DATA_DIR)
this.parser = new ChannelsParser({ storage: new Storage() })
this.options = options
this.configLoader = new ConfigLoader()
}
async create(): Promise<Queue> {
const channelsContent = await this.dataStorage.json('channels.json')
const channels = new Collection(channelsContent).map(data => new ApiChannel(data))
let index = 0
const queue = new Queue()
for (const channel of this.parsedChannels.all()) {
for (const channel of this.channels.all()) {
channel.index = index++
if (!channel.site || !channel.site_id || !channel.name) continue
if (this.options.lang && channel.lang !== this.options.lang) continue
const configPath = path.resolve(SITES_DIR, `${channel.site}/${channel.site}.config.js`)
const config: SiteConfig = await this.configLoader.load(configPath)
if (channel.xmltv_id) {
const found: ApiChannel = channels.first(
(_channel: ApiChannel) => _channel.id === channel.xmltv_id
)
if (found) {
channel.icon = found.logo
channel.name = found.name
}
} else {
if (!channel.xmltv_id) {
channel.xmltv_id = channel.site_id
}
-56
View File
@@ -1,56 +0,0 @@
import { Collection } from '@freearhey/core'
import { Channel } from 'epg-grabber'
export class XML {
items: Collection
constructor(items: Collection) {
this.items = items
}
toString() {
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.items.forEach((channel: Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id || ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
output += ` <channel site="${channel.site}" lang="${lang}" xmltv_id="${escapeString(
xmltv_id
)}" site_id="${site_id}"${logo}>${escapeString(channel.name)}</channel>\r\n`
})
output += '</channels>\r\n'
return output
}
}
function escapeString(value: string, defaultValue: string = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}
-28
View File
@@ -1,28 +0,0 @@
import { DateTime, Collection } from '@freearhey/core'
import { generateXMLTV } from 'epg-grabber'
type XMLTVProps = {
channels: Collection
programs: Collection
date: DateTime
}
export class XMLTV {
channels: Collection
programs: Collection
date: DateTime
constructor({ channels, programs, date }: XMLTVProps) {
this.channels = channels
this.programs = programs
this.date = date
}
toString() {
return generateXMLTV({
channels: this.channels.all(),
programs: this.programs.all(),
date: this.date.toJSON()
})
}
}
+164
View File
@@ -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()
}
}
}
+77
View File
@@ -0,0 +1,77 @@
import { Collection } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
export class ChannelList {
channels: Collection = new Collection()
constructor(data: { channels: epgGrabber.Channel[] }) {
this.channels = new Collection(data.channels)
}
add(channel: epgGrabber.Channel): this {
this.channels.add(channel)
return this
}
get(siteId: string): epgGrabber.Channel | undefined {
return this.channels.find((channel: epgGrabber.Channel) => channel.site_id == siteId)
}
sort(): this {
this.channels = this.channels.orderBy([
(channel: epgGrabber.Channel) => channel.lang || '_',
(channel: epgGrabber.Channel) => (channel.xmltv_id ? channel.xmltv_id.toLowerCase() : '0'),
(channel: epgGrabber.Channel) => channel.site_id
])
return this
}
toString() {
function escapeString(value: string, defaultValue = '') {
if (!value) return defaultValue
const regex = new RegExp(
'((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF' +
'FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD' +
'FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])' +
'|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\' +
'uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF' +
'[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
'(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
'g'
)
value = String(value || '').replace(regex, '')
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/\n|\r/g, ' ')
.replace(/ +/g, ' ')
.trim()
}
let output = '<?xml version="1.0" encoding="UTF-8"?>\r\n<channels>\r\n'
this.channels.forEach((channel: epgGrabber.Channel) => {
const logo = channel.logo ? ` logo="${channel.logo}"` : ''
const xmltv_id = channel.xmltv_id ? escapeString(channel.xmltv_id) : ''
const lang = channel.lang || ''
const site_id = channel.site_id || ''
const site = channel.site || ''
const displayName = channel.name ? escapeString(channel.name) : ''
output += ` <channel site="${site}" lang="${lang}" xmltv_id="${xmltv_id}" site_id="${site_id}"${logo}>${displayName}</channel>\r\n`
})
output += '</channels>\r\n'
return output
}
}
+124
View File
@@ -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 || ''
}
}
+35
View File
@@ -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()
})
}
}
+59
View File
@@ -0,0 +1,59 @@
import { Dictionary } from '@freearhey/core'
import epgGrabber from 'epg-grabber'
import { Feed, Channel } from '.'
export class GuideChannel {
channelId?: string
channel?: Channel
feedId?: string
feed?: Feed
xmltvId?: string
languageCode?: string
siteId?: string
logoUrl?: string
siteDomain?: string
siteName?: string
constructor(data: epgGrabber.Channel) {
const [channelId, feedId] = data.xmltv_id ? data.xmltv_id.split('@') : [undefined, undefined]
this.channelId = channelId
this.feedId = feedId
this.xmltvId = data.xmltv_id
this.languageCode = data.lang
this.siteId = data.site_id
this.logoUrl = data.logo
this.siteDomain = data.site
this.siteName = data.name
}
withChannel(channelsKeyById: Dictionary): this {
if (this.channelId) this.channel = channelsKeyById.get(this.channelId)
return this
}
withFeed(feedsKeyByStreamId: Dictionary): this {
if (this.feedId) this.feed = feedsKeyByStreamId.get(this.getStreamId())
return this
}
getStreamId(): string {
if (!this.channelId) return ''
if (!this.feedId) return this.channelId
return `${this.channelId}@${this.feedId}`
}
toJSON() {
return {
channel: this.channelId || null,
feed: this.feedId || null,
site: this.siteDomain || '',
site_id: this.siteId || '',
site_name: this.siteName || '',
lang: this.languageCode || ''
}
}
}
+9
View File
@@ -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'
+24
View File
@@ -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}`
}
}
+41
View File
@@ -0,0 +1,41 @@
import { Collection, type Dictionary } from '@freearhey/core'
import type { LogoData } from '../types/logo'
import { type Feed } from './feed'
export class Logo {
channelId?: string
feedId?: string
feed?: Feed
tags: Collection = new Collection()
width = 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}`
}
}
+63
View File
@@ -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())
}
}
+58
View File
@@ -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 }
}
+16
View File
@@ -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 []
}
}
+21
View File
@@ -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>
```
+3
View File
@@ -0,0 +1,3 @@
# Sites
_TABLE_
+38
View File
@@ -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([])
})
+27
View File
@@ -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[]
}
+20
View File
@@ -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[]
}
+16
View File
@@ -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
}
+12
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
export interface GuideData {
channel: string
feed: string
site: string
site_id: string
site_name: string
lang: string
}
+9
View File
@@ -0,0 +1,9 @@
export interface LogoData {
channel: string
feed: string | null
tags: string[]
width: number
height: number
format: string | null
url: string
}
+10
View File
@@ -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
}
+4 -3
View File
@@ -1,4 +1,6 @@
const { parser, url } = require('./9tv.co.il.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
@@ -18,8 +20,7 @@ it('can generate valid url', () => {
})
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>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.&#160;</div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>'
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
const result = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
@@ -49,7 +50,7 @@ it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: '<!DOCTYPE html><html><head></head><body></body></html>'
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
})
expect(result).toMatchObject([])
})
+1
View File
@@ -0,0 +1 @@
<li> <a href="#" class="guide_list_link w-inline-block"> <div class="guide_list_time">06:30</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=8484.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Слепая</h3> <div>Она не очень любит говорить о себе или о том, кто и зачем к ней обращается. Живет уединенно, в глуши. Но тех, кто приходит -принимает. Она видит судьбы.&#160;</div></div></div></a></li><li> <a href="#" class="guide_list_link even w-inline-block"> <div class="guide_list_time">09:10</div><div class="guide_info_group"> <div class="guide_info_pict" style="background-image: url(/download/pictures/img_id=23694.jpg);"></div><div class="guide_txt_group"> <h3 class="guide_info_title">Орел и решка. Морской сезон</h3> <div>Орел и решка. Морской сезон. Ведущие -Алина Астровская и Коля Серга.</div></div></div></a></li>
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><html><head></head><body></body></html>
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
>
-543
View File
@@ -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>
+27 -26
View File
@@ -19,7 +19,7 @@ module.exports = {
url({ date, channel }) {
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 }) {
let programs = []
@@ -41,43 +41,44 @@ module.exports = {
return programs
},
async channels() {
async channels({ region = 'syd' }) {
const now = dayjs()
const regions = [
'Sydney',
'Melbourne',
'Brisbane',
'GoldCoast',
'Perth',
'Adelaide',
'Hobart',
'Darwin',
'Canberra',
'New South Wales',
'Victoria',
'Townsville',
'Queensland',
'Western Australia',
'South Australia',
'Tasmania',
'Northern Territory'
]
const regions = {
syd: 'Sydney',
mel: 'Melbourne',
bri: 'Brisbane',
gc: 'GoldCoast',
per: 'Perth',
adl: 'Adelaide',
hbr: 'Hobart',
drw: 'Darwin',
cbr: 'Canberra',
nsw: 'New South Wales',
vic: 'Victoria',
tsv: 'Townsville',
qld: 'Queensland',
wa: 'Western Australia',
sa: 'South Australia',
tas: 'Tasmania',
nt: 'Northern Territory'
}
let channels = []
for (let region of regions) {
const regionName = regions[region]
const data = await axios
.get(`https://epg.abctv.net.au/processed/${region}_${now.format('YYYY-MM-DD')}.json`)
.get(
`https://cdn.iview.abc.net.au/epg/processed/${regionName}_${now.format('YYYY-MM-DD')}.json`
)
.then(r => r.data)
.catch(console.log)
for (let item of data.schedule) {
channels.push({
lang: 'en',
site_id: `${region}#${item.channel}`,
site_id: `${regionName}#${item.channel}`,
name: item.channel
})
}
}
return channels
}
@@ -92,7 +93,7 @@ function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#')
const channelData = data.schedule.find(i => i.channel == channelId)
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
} catch (err) {
} catch {
return []
}
}
+28 -31
View File
@@ -1,54 +1,51 @@
const { parser, url } = require('./abc.net.au.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const date = dayjs.utc('2022-12-22', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: 'Sydney#ABC1',
xmltv_id: 'ABCTV.au'
}
const date = dayjs.utc('2025-02-04', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'Sydney#ABC1' }
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}]}]}'
const result = parser({ content, channel }).map(p => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, channel }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
title: 'Silent Witness',
sub_title: 'Lift Up Your Hearts (part Two)',
expect(results.length).toBe(30)
expect(results[0]).toMatchObject({
title: "Julia Zemiro's Home Delivery",
sub_title: 'Maggie Beer',
description:
'When tragedy strikes close to home, it puts head teacher Noah Taylor on a collision course with the criminals responsible. Can the Lyell team help him stop the cycle of violence?',
category: ['Entertainment'],
"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', 'Factual'],
rating: {
system: 'ACB',
value: 'M'
value: 'G'
},
season: 22,
episode: 4,
image: 'https://www.abc.net.au/tv/common/images/publicity/ZW2178A004S00_460.jpg',
start: '2022-12-21T13:46:00.000Z',
stop: '2022-12-21T14:44:00.000Z'
}
])
season: null,
episode: null,
image: 'https://www.abc.net.au/tv/common/images/publicity/LE1761H002S00_460.jpg',
start: '2025-02-03T12:40:00.000Z',
stop: '2025-02-03T13:09:00.000Z'
})
})
it('can handle empty guide', () => {
const result = parser(
{
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>'
},
const results = parser({
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')),
channel
)
expect(result).toMatchObject([])
})
expect(results).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>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<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#ABC1">ABC1</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="Tasmania#YOUTV">YOUTV</channel>
</channels>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<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>
</channels>

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