mirror of
https://github.com/iptv-org/iptv.git
synced 2025-05-12 10:00:05 -04:00
Merge pull request #21231 from iptv-org/patch-2025.04.2
Patch 2025.04.2
This commit is contained in:
commit
cf930921a3
147 changed files with 3539 additions and 2440 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -3,7 +3,7 @@ node_modules
|
||||||
.secrets
|
.secrets
|
||||||
.actrc
|
.actrc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.gh-pages
|
/.gh-pages/
|
||||||
.api
|
/.api/
|
||||||
.env
|
.env
|
||||||
/temp
|
/temp/
|
|
@ -93,12 +93,12 @@ Same thing, but split up into separate files:
|
||||||
|
|
||||||
### Grouped by region
|
### Grouped by region
|
||||||
|
|
||||||
|
Playlists in which channels are grouped by the region for which they are broadcasted.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
Playlists in which channels are grouped by the region for which they are broadcasted.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
https://iptv-org.github.io/iptv/index.region.m3u
|
https://iptv-org.github.io/iptv/index.region.m3u
|
||||||
```
|
```
|
||||||
|
|
|
@ -176,6 +176,7 @@ To run scripts use the `npm run <script-name>` command.
|
||||||
- `playlist:validate`: сhecks ids and links in internal playlists for errors.
|
- `playlist:validate`: сhecks ids and links in internal playlists for errors.
|
||||||
- `playlist:lint`: сhecks internal playlists for syntax errors.
|
- `playlist:lint`: сhecks internal playlists for syntax errors.
|
||||||
- `playlist:test`: tests links in internal playlists.
|
- `playlist:test`: tests links in internal playlists.
|
||||||
|
- `playlist:edit`: utility for quick streams mapping.
|
||||||
- `playlist:deploy`: allows to manually publish all generated via `playlist:generate` playlists. To run the script you must provide your [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with write access to the repository.
|
- `playlist:deploy`: allows to manually publish all generated via `playlist:generate` playlists. To run the script you must provide your [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with write access to the repository.
|
||||||
- `readme:update`: updates the list of playlists in [README.md](README.md).
|
- `readme:update`: updates the list of playlists in [README.md](README.md).
|
||||||
- `report:create`: creates a report on current issues.
|
- `report:create`: creates a report on current issues.
|
||||||
|
|
1390
package-lock.json
generated
1390
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,6 +13,7 @@
|
||||||
"playlist:validate": "tsx scripts/commands/playlist/validate.ts",
|
"playlist:validate": "tsx scripts/commands/playlist/validate.ts",
|
||||||
"playlist:lint": "npx m3u-linter -c m3u-linter.json",
|
"playlist:lint": "npx m3u-linter -c m3u-linter.json",
|
||||||
"playlist:test": "tsx scripts/commands/playlist/test.ts",
|
"playlist:test": "tsx scripts/commands/playlist/test.ts",
|
||||||
|
"playlist:edit": "tsx scripts/commands/playlist/edit.ts",
|
||||||
"playlist:deploy": "npx gh-pages-clean && npx gh-pages -m \"Deploy to GitHub Pages\" -d .gh-pages -r https://$GITHUB_TOKEN@github.com/iptv-org/iptv.git",
|
"playlist:deploy": "npx gh-pages-clean && npx gh-pages -m \"Deploy to GitHub Pages\" -d .gh-pages -r https://$GITHUB_TOKEN@github.com/iptv-org/iptv.git",
|
||||||
"readme:update": "tsx scripts/commands/readme/update.ts",
|
"readme:update": "tsx scripts/commands/readme/update.ts",
|
||||||
"report:create": "tsx scripts/commands/report/create.ts",
|
"report:create": "tsx scripts/commands/report/create.ts",
|
||||||
|
@ -39,12 +40,15 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.0",
|
"@eslint/eslintrc": "^3.3.0",
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@freearhey/core": "^0.7.0",
|
"@freearhey/core": "^0.8.2",
|
||||||
|
"@freearhey/search-js": "^0.1.2",
|
||||||
|
"@inquirer/prompts": "^7.4.1",
|
||||||
"@octokit/core": "^6.1.4",
|
"@octokit/core": "^6.1.4",
|
||||||
"@octokit/plugin-paginate-rest": "^11.4.3",
|
"@octokit/plugin-paginate-rest": "^11.4.3",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^7.1.3",
|
"@octokit/plugin-rest-endpoint-methods": "^7.1.3",
|
||||||
"@octokit/types": "^11.1.0",
|
"@octokit/types": "^11.1.0",
|
||||||
"@types/cli-progress": "^3.11.3",
|
"@types/cli-progress": "^3.11.3",
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/lodash": "^4.14.198",
|
"@types/lodash": "^4.14.198",
|
||||||
"@types/numeral": "^2.0.3",
|
"@types/numeral": "^2.0.3",
|
||||||
|
@ -58,6 +62,7 @@
|
||||||
"commander": "^8.3.0",
|
"commander": "^8.3.0",
|
||||||
"console-table-printer": "^2.12.1",
|
"console-table-printer": "^2.12.1",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
|
"glob": "^11.0.2",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"iptv-checker": "^0.29.1",
|
"iptv-checker": "^0.29.1",
|
||||||
"iptv-playlist-parser": "^0.13.0",
|
"iptv-playlist-parser": "^0.13.0",
|
||||||
|
@ -65,8 +70,8 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"m3u-linter": "^0.4.2",
|
"m3u-linter": "^0.4.2",
|
||||||
"markdown-include": "^0.4.3",
|
"markdown-include": "^0.4.3",
|
||||||
|
"node-cleanup": "^2.1.2",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"transliteration": "^2.3.5",
|
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"tsx": "^4.6.2",
|
"tsx": "^4.6.2",
|
||||||
"valid-url": "^1.0.9"
|
"valid-url": "^1.0.9"
|
||||||
|
|
|
@ -1,30 +1,25 @@
|
||||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
||||||
|
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import { API_DIR, STREAMS_DIR, DATA_DIR } from '../../constants'
|
import { API_DIR, STREAMS_DIR, DATA_DIR } from '../../constants'
|
||||||
import { PlaylistParser } from '../../core'
|
import type { DataLoaderData } from '../../types/dataLoader'
|
||||||
import { Stream, Channel, Feed } from '../../models'
|
import { Logger, Storage } from '@freearhey/core'
|
||||||
import { uniqueId } from 'lodash'
|
import { Stream } from '../../models'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.info('loading api data...')
|
logger.info('loading data from api...')
|
||||||
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const channelsData = await dataStorage.json('channels.json')
|
const dataLoader = new DataLoader({ storage: dataStorage })
|
||||||
const channels = new Collection(channelsData).map(data => new Channel(data))
|
const data: DataLoaderData = await dataLoader.load()
|
||||||
const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id)
|
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||||
const feedsData = await dataStorage.json('feeds.json')
|
|
||||||
const feeds = new Collection(feedsData).map(data =>
|
|
||||||
new Feed(data).withChannel(channelsGroupedById)
|
|
||||||
)
|
|
||||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) =>
|
|
||||||
feed.channel ? feed.channel.id : uniqueId()
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage: streamsStorage,
|
storage: streamsStorage,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
})
|
})
|
||||||
const files = await streamsStorage.list('**/*.m3u')
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import { Logger } from '@freearhey/core'
|
import { DATA_DIR } from '../../constants'
|
||||||
import { ApiClient } from '../../core'
|
import { Storage } from '@freearhey/core'
|
||||||
|
import { DataLoader } from '../../core'
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const storage = new Storage(DATA_DIR)
|
||||||
const client = new ApiClient({ logger })
|
const loader = new DataLoader({ storage })
|
||||||
|
|
||||||
const requests = [
|
await Promise.all([
|
||||||
client.download('blocklist.json'),
|
loader.download('blocklist.json'),
|
||||||
client.download('categories.json'),
|
loader.download('categories.json'),
|
||||||
client.download('channels.json'),
|
loader.download('channels.json'),
|
||||||
client.download('countries.json'),
|
loader.download('countries.json'),
|
||||||
client.download('languages.json'),
|
loader.download('languages.json'),
|
||||||
client.download('regions.json'),
|
loader.download('regions.json'),
|
||||||
client.download('subdivisions.json'),
|
loader.download('subdivisions.json'),
|
||||||
client.download('feeds.json'),
|
loader.download('feeds.json'),
|
||||||
client.download('timezones.json')
|
loader.download('timezones.json'),
|
||||||
]
|
loader.download('guides.json'),
|
||||||
|
loader.download('streams.json')
|
||||||
await Promise.all(requests)
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
208
scripts/commands/playlist/edit.ts
Normal file
208
scripts/commands/playlist/edit.ts
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import { Storage, Collection, Logger, Dictionary } from '@freearhey/core'
|
||||||
|
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
||||||
|
import type { ChannelSearchableData } from '../../types/channel'
|
||||||
|
import { Channel, Feed, Playlist, Stream } from '../../models'
|
||||||
|
import { DataProcessorData } from '../../types/dataProcessor'
|
||||||
|
import { DataLoaderData } from '../../types/dataLoader'
|
||||||
|
import { select, input } from '@inquirer/prompts'
|
||||||
|
import { DATA_DIR } from '../../constants'
|
||||||
|
import nodeCleanup from 'node-cleanup'
|
||||||
|
import sjs from '@freearhey/search-js'
|
||||||
|
import { Command } from 'commander'
|
||||||
|
import readline from 'readline'
|
||||||
|
|
||||||
|
type ChoiceValue = { type: string; value?: Feed | Channel }
|
||||||
|
type 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 parsedStreams = new Collection()
|
||||||
|
|
||||||
|
main(filepath)
|
||||||
|
nodeCleanup(() => {
|
||||||
|
save(filepath)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 streams...')
|
||||||
|
const parser = new PlaylistParser({ storage, feedsGroupedByChannelId, channelsKeyById })
|
||||||
|
parsedStreams = await parser.parseFile(filepath)
|
||||||
|
const streamsWithoutId = parsedStreams.filter((stream: Stream) => !stream.id)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`found ${parsedStreams.count()} streams (including ${streamsWithoutId.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 stream of streamsWithoutId.all()) {
|
||||||
|
try {
|
||||||
|
stream.id = await selectChannel(stream, searchIndex, feedsGroupedByChannelId, channelsKeyById)
|
||||||
|
} catch (err) {
|
||||||
|
logger.info(err.message)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamsWithoutId.forEach((stream: Stream) => {
|
||||||
|
if (stream.id === '-') {
|
||||||
|
stream.id = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectChannel(
|
||||||
|
stream: Stream,
|
||||||
|
searchIndex,
|
||||||
|
feedsGroupedByChannelId: Dictionary,
|
||||||
|
channelsKeyById: Dictionary
|
||||||
|
): Promise<string> {
|
||||||
|
const query = escapeRegex(stream.getName())
|
||||||
|
const similarChannels = searchIndex
|
||||||
|
.search(query)
|
||||||
|
.map((item: ChannelSearchableData) => channelsKeyById.get(item.id))
|
||||||
|
|
||||||
|
const url = stream.url.length > 50 ? stream.url.slice(0, 50) + '...' : stream.url
|
||||||
|
|
||||||
|
const selected: ChoiceValue = await select({
|
||||||
|
message: `Select channel ID for "${stream.name}" (${url}):`,
|
||||||
|
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 = 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.altNames.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) {
|
||||||
|
if (!storage.existsSync(filepath)) return
|
||||||
|
const playlist = new Playlist(parsedStreams)
|
||||||
|
storage.saveSync(filepath, playlist.toString())
|
||||||
|
logger.info(`\nFile '${filepath}' successfully saved`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(string: string) {
|
||||||
|
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
|
}
|
|
@ -1,33 +1,28 @@
|
||||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
import { Logger, Storage } from '@freearhey/core'
|
||||||
import { STREAMS_DIR, DATA_DIR } from '../../constants'
|
import { STREAMS_DIR, DATA_DIR } from '../../constants'
|
||||||
import { PlaylistParser } from '../../core'
|
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
||||||
import { Stream, Playlist, Channel, Feed } from '../../models'
|
import { Stream, Playlist } from '../../models'
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import { uniqueId } from 'lodash'
|
import { DataLoaderData } from '../../types/dataLoader'
|
||||||
|
import { DataProcessorData } from '../../types/dataProcessor'
|
||||||
|
|
||||||
program.argument('[filepath]', 'Path to file to validate').parse(process.argv)
|
program.argument('[filepath]', 'Path to file to validate').parse(process.argv)
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const channelsData = await dataStorage.json('channels.json')
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const channels = new Collection(channelsData).map(data => new Channel(data))
|
const data: DataLoaderData = await loader.load()
|
||||||
const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id)
|
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||||
const feedsData = await dataStorage.json('feeds.json')
|
|
||||||
const feeds = new Collection(feedsData).map(data =>
|
|
||||||
new Feed(data).withChannel(channelsGroupedById)
|
|
||||||
)
|
|
||||||
const feedsGroupedByChannelId = feeds.groupBy(feed =>
|
|
||||||
feed.channel ? feed.channel.id : uniqueId()
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage: streamsStorage,
|
storage: streamsStorage,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
})
|
})
|
||||||
const files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u')
|
const files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u')
|
||||||
|
@ -46,7 +41,7 @@ async function main() {
|
||||||
|
|
||||||
logger.info('removing wrong id...')
|
logger.info('removing wrong id...')
|
||||||
streams = streams.map((stream: Stream) => {
|
streams = streams.map((stream: Stream) => {
|
||||||
if (!stream.channel || channelsGroupedById.missing(stream.channel.id)) {
|
if (!stream.channel || channelsKeyById.missing(stream.channel.id)) {
|
||||||
stream.id = ''
|
stream.id = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,109 +1,47 @@
|
||||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
import { PlaylistParser, DataProcessor, DataLoader } from '../../core'
|
||||||
import { PlaylistParser } from '../../core'
|
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import {
|
import { DATA_DIR, LOGS_DIR, STREAMS_DIR } from '../../constants'
|
||||||
Stream,
|
import type { DataLoaderData } from '../../types/dataLoader'
|
||||||
Category,
|
import { Logger, Storage, File } from '@freearhey/core'
|
||||||
Channel,
|
import { Stream } from '../../models'
|
||||||
Language,
|
|
||||||
Country,
|
|
||||||
Region,
|
|
||||||
Subdivision,
|
|
||||||
Feed,
|
|
||||||
Timezone
|
|
||||||
} from '../../models'
|
|
||||||
import { uniqueId } from 'lodash'
|
import { uniqueId } from 'lodash'
|
||||||
import {
|
import {
|
||||||
|
IndexCategoryGenerator,
|
||||||
|
IndexLanguageGenerator,
|
||||||
|
IndexCountryGenerator,
|
||||||
|
IndexRegionGenerator,
|
||||||
CategoriesGenerator,
|
CategoriesGenerator,
|
||||||
CountriesGenerator,
|
CountriesGenerator,
|
||||||
LanguagesGenerator,
|
LanguagesGenerator,
|
||||||
RegionsGenerator,
|
RegionsGenerator,
|
||||||
IndexGenerator,
|
IndexGenerator
|
||||||
IndexCategoryGenerator,
|
|
||||||
IndexCountryGenerator,
|
|
||||||
IndexLanguageGenerator,
|
|
||||||
IndexRegionGenerator
|
|
||||||
} from '../../generators'
|
} from '../../generators'
|
||||||
import { DATA_DIR, LOGS_DIR, STREAMS_DIR } from '../../constants'
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const logFile = new File('generators.log')
|
||||||
const generatorsLogger = new Logger({
|
|
||||||
stream: await new Storage(LOGS_DIR).createStream(`generators.log`)
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
const categoriesData = await dataStorage.json('categories.json')
|
const processor = new DataProcessor()
|
||||||
const countriesData = await dataStorage.json('countries.json')
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const languagesData = await dataStorage.json('languages.json')
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const regionsData = await dataStorage.json('regions.json')
|
const data: DataLoaderData = await loader.load()
|
||||||
const subdivisionsData = await dataStorage.json('subdivisions.json')
|
const {
|
||||||
const timezonesData = await dataStorage.json('timezones.json')
|
feedsGroupedByChannelId,
|
||||||
const channelsData = await dataStorage.json('channels.json')
|
channelsKeyById,
|
||||||
const feedsData = await dataStorage.json('feeds.json')
|
categories,
|
||||||
|
countries,
|
||||||
logger.info('preparing data...')
|
regions
|
||||||
const subdivisions = new Collection(subdivisionsData).map(data => new Subdivision(data))
|
}: DataProcessorData = processor.process(data)
|
||||||
const subdivisionsGroupedByCode = subdivisions.keyBy(
|
|
||||||
(subdivision: Subdivision) => subdivision.code
|
|
||||||
)
|
|
||||||
const subdivisionsGroupedByCountryCode = subdivisions.groupBy(
|
|
||||||
(subdivision: Subdivision) => subdivision.countryCode
|
|
||||||
)
|
|
||||||
let regions = new Collection(regionsData).map(data =>
|
|
||||||
new Region(data).withSubdivisions(subdivisions)
|
|
||||||
)
|
|
||||||
const regionsGroupedByCode = regions.keyBy((region: Region) => region.code)
|
|
||||||
const categories = new Collection(categoriesData).map(data => new Category(data))
|
|
||||||
const categoriesGroupedById = categories.keyBy((category: Category) => category.id)
|
|
||||||
const languages = new Collection(languagesData).map(data => new Language(data))
|
|
||||||
const languagesGroupedByCode = languages.keyBy((language: Language) => language.code)
|
|
||||||
const countries = new Collection(countriesData).map(data =>
|
|
||||||
new Country(data)
|
|
||||||
.withRegions(regions)
|
|
||||||
.withLanguage(languagesGroupedByCode)
|
|
||||||
.withSubdivisions(subdivisionsGroupedByCountryCode)
|
|
||||||
)
|
|
||||||
const countriesGroupedByCode = countries.keyBy((country: Country) => country.code)
|
|
||||||
regions = regions.map((region: Region) => region.withCountries(countriesGroupedByCode))
|
|
||||||
|
|
||||||
const timezones = new Collection(timezonesData).map(data =>
|
|
||||||
new Timezone(data).withCountries(countriesGroupedByCode)
|
|
||||||
)
|
|
||||||
const timezonesGroupedById = timezones.keyBy((timezone: Timezone) => timezone.id)
|
|
||||||
|
|
||||||
const channels = new Collection(channelsData).map(data =>
|
|
||||||
new Channel(data)
|
|
||||||
.withCategories(categoriesGroupedById)
|
|
||||||
.withCountry(countriesGroupedByCode)
|
|
||||||
.withSubdivision(subdivisionsGroupedByCode)
|
|
||||||
)
|
|
||||||
const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id)
|
|
||||||
const feeds = new Collection(feedsData).map(data =>
|
|
||||||
new Feed(data)
|
|
||||||
.withChannel(channelsGroupedById)
|
|
||||||
.withLanguages(languagesGroupedByCode)
|
|
||||||
.withTimezones(timezonesGroupedById)
|
|
||||||
.withBroadcastCountries(
|
|
||||||
countriesGroupedByCode,
|
|
||||||
regionsGroupedByCode,
|
|
||||||
subdivisionsGroupedByCode
|
|
||||||
)
|
|
||||||
.withBroadcastRegions(regions)
|
|
||||||
.withBroadcastSubdivisions(subdivisionsGroupedByCode)
|
|
||||||
)
|
|
||||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) =>
|
|
||||||
feed.channel ? feed.channel.id : uniqueId()
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
const storage = new Storage(STREAMS_DIR)
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage,
|
storage: streamsStorage,
|
||||||
channelsGroupedById,
|
feedsGroupedByChannelId,
|
||||||
feedsGroupedByChannelId
|
channelsKeyById
|
||||||
})
|
})
|
||||||
const files = await storage.list('**/*.m3u')
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
let streams = await parser.parse(files)
|
let streams = await parser.parse(files)
|
||||||
const totalStreams = streams.count()
|
const totalStreams = streams.count()
|
||||||
streams = streams.uniqBy((stream: Stream) =>
|
streams = streams.uniqBy((stream: Stream) =>
|
||||||
|
@ -122,42 +60,46 @@ async function main() {
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info('generating categories/...')
|
logger.info('generating categories/...')
|
||||||
await new CategoriesGenerator({ categories, streams, logger: generatorsLogger }).generate()
|
await new CategoriesGenerator({ categories, streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('generating countries/...')
|
logger.info('generating countries/...')
|
||||||
await new CountriesGenerator({
|
await new CountriesGenerator({
|
||||||
countries,
|
countries,
|
||||||
streams,
|
streams,
|
||||||
logger: generatorsLogger
|
logFile
|
||||||
}).generate()
|
}).generate()
|
||||||
|
|
||||||
logger.info('generating languages/...')
|
logger.info('generating languages/...')
|
||||||
await new LanguagesGenerator({ streams, logger: generatorsLogger }).generate()
|
await new LanguagesGenerator({ streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('generating regions/...')
|
logger.info('generating regions/...')
|
||||||
await new RegionsGenerator({
|
await new RegionsGenerator({
|
||||||
streams,
|
streams,
|
||||||
regions,
|
regions,
|
||||||
logger: generatorsLogger
|
logFile
|
||||||
}).generate()
|
}).generate()
|
||||||
|
|
||||||
logger.info('generating index.m3u...')
|
logger.info('generating index.m3u...')
|
||||||
await new IndexGenerator({ streams, logger: generatorsLogger }).generate()
|
await new IndexGenerator({ streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('generating index.category.m3u...')
|
logger.info('generating index.category.m3u...')
|
||||||
await new IndexCategoryGenerator({ streams, logger: generatorsLogger }).generate()
|
await new IndexCategoryGenerator({ streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('generating index.country.m3u...')
|
logger.info('generating index.country.m3u...')
|
||||||
await new IndexCountryGenerator({
|
await new IndexCountryGenerator({
|
||||||
streams,
|
streams,
|
||||||
logger: generatorsLogger
|
logFile
|
||||||
}).generate()
|
}).generate()
|
||||||
|
|
||||||
logger.info('generating index.language.m3u...')
|
logger.info('generating index.language.m3u...')
|
||||||
await new IndexLanguageGenerator({ streams, logger: generatorsLogger }).generate()
|
await new IndexLanguageGenerator({ streams, logFile }).generate()
|
||||||
|
|
||||||
logger.info('generating index.region.m3u...')
|
logger.info('generating index.region.m3u...')
|
||||||
await new IndexRegionGenerator({ streams, regions, logger: generatorsLogger }).generate()
|
await new IndexRegionGenerator({ streams, regions, logFile }).generate()
|
||||||
|
|
||||||
|
logger.info('saving generators.log...')
|
||||||
|
const logStorage = new Storage(LOGS_DIR)
|
||||||
|
logStorage.saveFile(logFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { Logger, Storage, Collection } from '@freearhey/core'
|
import { Logger, Storage, Collection } from '@freearhey/core'
|
||||||
import { ROOT_DIR, STREAMS_DIR, DATA_DIR } from '../../constants'
|
import { ROOT_DIR, STREAMS_DIR, DATA_DIR } from '../../constants'
|
||||||
import { PlaylistParser, StreamTester, CliTable } from '../../core'
|
import { PlaylistParser, StreamTester, CliTable, DataProcessor, DataLoader } from '../../core'
|
||||||
import { Stream, Feed, Channel } from '../../models'
|
import { Stream } from '../../models'
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import { eachLimit } from 'async-es'
|
import { eachLimit } from 'async-es'
|
||||||
import commandExists from 'command-exists'
|
import commandExists from 'command-exists'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
import dns from 'node:dns'
|
import dns from 'node:dns'
|
||||||
|
import type { DataLoaderData } from '../../types/dataLoader'
|
||||||
|
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||||
|
|
||||||
const cpus = os.cpus()
|
const cpus = os.cpus()
|
||||||
|
|
||||||
|
@ -54,22 +56,18 @@ async function main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('loading channels from api...')
|
logger.info('loading data from api...')
|
||||||
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const channelsData = await dataStorage.json('channels.json')
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const channels = new Collection(channelsData).map(data => new Channel(data))
|
const data: DataLoaderData = await loader.load()
|
||||||
const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id)
|
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||||
const feedsData = await dataStorage.json('feeds.json')
|
|
||||||
const feeds = new Collection(feedsData).map(data =>
|
|
||||||
new Feed(data).withChannel(channelsGroupedById)
|
|
||||||
)
|
|
||||||
const feedsGroupedByChannelId = feeds.groupBy(feed => feed.channel)
|
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
const rootStorage = new Storage(ROOT_DIR)
|
const rootStorage = new Storage(ROOT_DIR)
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage: rootStorage,
|
storage: rootStorage,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
})
|
})
|
||||||
const files = program.args.length ? program.args : await rootStorage.list(`${STREAMS_DIR}/*.m3u`)
|
const files = program.args.length ? program.args : await rootStorage.list(`${STREAMS_DIR}/*.m3u`)
|
||||||
|
@ -156,15 +154,24 @@ function drawTable() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFinish() {
|
function onFinish(error) {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
drawTable()
|
drawTable()
|
||||||
|
|
||||||
logger.error(`\n${errors + warnings} problems (${errors} errors, ${warnings} warnings)`)
|
if (errors > 0 || warnings > 0) {
|
||||||
|
console.log(
|
||||||
|
chalk.red(`\n${errors + warnings} problems (${errors} errors, ${warnings} warnings)`)
|
||||||
|
)
|
||||||
|
|
||||||
if (errors > 0) {
|
if (errors > 0) {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
|
|
@ -1,38 +1,33 @@
|
||||||
|
import { DataLoader, DataProcessor, IssueLoader, PlaylistParser } from '../../core'
|
||||||
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
||||||
|
import type { DataProcessorData } from '../../types/dataProcessor'
|
||||||
|
import { Stream, Playlist, Channel, Issue } from '../../models'
|
||||||
|
import type { DataLoaderData } from '../../types/dataLoader'
|
||||||
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
||||||
import { IssueLoader, PlaylistParser } from '../../core'
|
|
||||||
import { Stream, Playlist, Channel, Feed, Issue } from '../../models'
|
|
||||||
import validUrl from 'valid-url'
|
import validUrl from 'valid-url'
|
||||||
import { uniqueId } from 'lodash'
|
|
||||||
|
|
||||||
let processedIssues = new Collection()
|
let processedIssues = new Collection()
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger({ disabled: true })
|
const logger = new Logger({ disabled: true })
|
||||||
const loader = new IssueLoader()
|
const issueLoader = new IssueLoader()
|
||||||
|
|
||||||
logger.info('loading issues...')
|
logger.info('loading issues...')
|
||||||
const issues = await loader.load()
|
const issues = await issueLoader.load()
|
||||||
|
|
||||||
logger.info('loading channels from api...')
|
logger.info('loading data from api...')
|
||||||
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const channelsData = await dataStorage.json('channels.json')
|
const dataLoader = new DataLoader({ storage: dataStorage })
|
||||||
const channels = new Collection(channelsData).map(data => new Channel(data))
|
const data: DataLoaderData = await dataLoader.load()
|
||||||
const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id)
|
const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data)
|
||||||
const feedsData = await dataStorage.json('feeds.json')
|
|
||||||
const feeds = new Collection(feedsData).map(data =>
|
|
||||||
new Feed(data).withChannel(channelsGroupedById)
|
|
||||||
)
|
|
||||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) =>
|
|
||||||
feed.channel ? feed.channel.id : uniqueId()
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage: streamsStorage,
|
storage: streamsStorage,
|
||||||
feedsGroupedByChannelId,
|
feedsGroupedByChannelId,
|
||||||
channelsGroupedById
|
channelsKeyById
|
||||||
})
|
})
|
||||||
const files = await streamsStorage.list('**/*.m3u')
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
const streams = await parser.parse(files)
|
const streams = await parser.parse(files)
|
||||||
|
@ -44,7 +39,7 @@ async function main() {
|
||||||
await editStreams({
|
await editStreams({
|
||||||
streams,
|
streams,
|
||||||
issues,
|
issues,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -52,7 +47,7 @@ async function main() {
|
||||||
await addStreams({
|
await addStreams({
|
||||||
streams,
|
streams,
|
||||||
issues,
|
issues,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -101,12 +96,12 @@ async function removeStreams({ streams, issues }: { streams: Collection; issues:
|
||||||
async function editStreams({
|
async function editStreams({
|
||||||
streams,
|
streams,
|
||||||
issues,
|
issues,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
}: {
|
}: {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
issues: Collection
|
issues: Collection
|
||||||
channelsGroupedById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
feedsGroupedByChannelId: Dictionary
|
feedsGroupedByChannelId: Dictionary
|
||||||
}) {
|
}) {
|
||||||
const requests = issues.filter(
|
const requests = issues.filter(
|
||||||
|
@ -129,7 +124,7 @@ async function editStreams({
|
||||||
stream
|
stream
|
||||||
.setChannelId(channelId)
|
.setChannelId(channelId)
|
||||||
.setFeedId(feedId)
|
.setFeedId(feedId)
|
||||||
.withChannel(channelsGroupedById)
|
.withChannel(channelsKeyById)
|
||||||
.withFeed(feedsGroupedByChannelId)
|
.withFeed(feedsGroupedByChannelId)
|
||||||
.updateId()
|
.updateId()
|
||||||
.updateName()
|
.updateName()
|
||||||
|
@ -143,8 +138,8 @@ async function editStreams({
|
||||||
|
|
||||||
if (data.has('label')) stream.setLabel(label)
|
if (data.has('label')) stream.setLabel(label)
|
||||||
if (data.has('quality')) stream.setQuality(quality)
|
if (data.has('quality')) stream.setQuality(quality)
|
||||||
if (data.has('httpUserAgent')) stream.setHttpUserAgent(httpUserAgent)
|
if (data.has('httpUserAgent')) stream.setUserAgent(httpUserAgent)
|
||||||
if (data.has('httpReferrer')) stream.setHttpReferrer(httpReferrer)
|
if (data.has('httpReferrer')) stream.setReferrer(httpReferrer)
|
||||||
|
|
||||||
processedIssues.add(issue.number)
|
processedIssues.add(issue.number)
|
||||||
})
|
})
|
||||||
|
@ -153,12 +148,12 @@ async function editStreams({
|
||||||
async function addStreams({
|
async function addStreams({
|
||||||
streams,
|
streams,
|
||||||
issues,
|
issues,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
}: {
|
}: {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
issues: Collection
|
issues: Collection
|
||||||
channelsGroupedById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
feedsGroupedByChannelId: Dictionary
|
feedsGroupedByChannelId: Dictionary
|
||||||
}) {
|
}) {
|
||||||
const requests = issues.filter(
|
const requests = issues.filter(
|
||||||
|
@ -168,51 +163,32 @@ async function addStreams({
|
||||||
const data = issue.data
|
const data = issue.data
|
||||||
if (data.missing('streamId') || data.missing('streamUrl')) return
|
if (data.missing('streamId') || data.missing('streamUrl')) return
|
||||||
if (streams.includes((_stream: Stream) => _stream.url === data.getString('streamUrl'))) return
|
if (streams.includes((_stream: Stream) => _stream.url === data.getString('streamUrl'))) return
|
||||||
const stringUrl = data.getString('streamUrl') || ''
|
const streamUrl = data.getString('streamUrl') || ''
|
||||||
if (!isUri(stringUrl)) return
|
if (!isUri(streamUrl)) return
|
||||||
|
|
||||||
const streamId = data.getString('streamId') || ''
|
const streamId = data.getString('streamId') || ''
|
||||||
const [channelId] = streamId.split('@')
|
const [channelId, feedId] = streamId.split('@')
|
||||||
|
|
||||||
const channel: Channel = channelsGroupedById.get(channelId)
|
const channel: Channel = channelsKeyById.get(channelId)
|
||||||
if (!channel) return
|
if (!channel) return
|
||||||
|
|
||||||
const label = data.getString('label') || ''
|
const label = data.getString('label') || null
|
||||||
const quality = data.getString('quality') || ''
|
const quality = data.getString('quality') || null
|
||||||
const httpUserAgent = data.getString('httpUserAgent') || ''
|
const httpUserAgent = data.getString('httpUserAgent') || null
|
||||||
const httpReferrer = data.getString('httpReferrer') || ''
|
const httpReferrer = data.getString('httpReferrer') || null
|
||||||
|
|
||||||
const stream = new Stream({
|
const stream = new Stream({
|
||||||
tvg: {
|
channel: channelId,
|
||||||
id: streamId,
|
feed: feedId,
|
||||||
name: '',
|
|
||||||
url: '',
|
|
||||||
logo: '',
|
|
||||||
rec: '',
|
|
||||||
shift: ''
|
|
||||||
},
|
|
||||||
name: data.getString('channelName') || channel.name,
|
name: data.getString('channelName') || channel.name,
|
||||||
url: stringUrl,
|
url: streamUrl,
|
||||||
group: {
|
user_agent: httpUserAgent,
|
||||||
title: ''
|
referrer: httpReferrer,
|
||||||
},
|
quality,
|
||||||
http: {
|
label
|
||||||
'user-agent': httpUserAgent,
|
|
||||||
referrer: httpReferrer
|
|
||||||
},
|
|
||||||
line: -1,
|
|
||||||
raw: '',
|
|
||||||
timeshift: '',
|
|
||||||
catchup: {
|
|
||||||
type: '',
|
|
||||||
source: '',
|
|
||||||
days: ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.withChannel(channelsGroupedById)
|
.withChannel(channelsKeyById)
|
||||||
.withFeed(feedsGroupedByChannelId)
|
.withFeed(feedsGroupedByChannelId)
|
||||||
.setLabel(label)
|
|
||||||
.setQuality(quality)
|
|
||||||
.updateName()
|
.updateName()
|
||||||
.updateFilepath()
|
.updateFilepath()
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
||||||
import { PlaylistParser } from '../../core'
|
import { DataLoader, DataProcessor, PlaylistParser } from '../../core'
|
||||||
import { Channel, Stream, Blocked, Feed } from '../../models'
|
import { DataProcessorData } from '../../types/dataProcessor'
|
||||||
|
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
||||||
|
import { DataLoaderData } from '../../types/dataLoader'
|
||||||
|
import { BlocklistRecord, Stream } from '../../models'
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import { uniqueId } from 'lodash'
|
|
||||||
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
|
||||||
|
|
||||||
program.argument('[filepath]', 'Path to file to validate').parse(process.argv)
|
program.argument('[filepath]', 'Path to file to validate').parse(process.argv)
|
||||||
|
|
||||||
|
@ -18,26 +19,21 @@ async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const channelsData = await dataStorage.json('channels.json')
|
const loader = new DataLoader({ storage: dataStorage })
|
||||||
const channels = new Collection(channelsData).map(data => new Channel(data))
|
const data: DataLoaderData = await loader.load()
|
||||||
const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id)
|
const {
|
||||||
const feedsData = await dataStorage.json('feeds.json')
|
channelsKeyById,
|
||||||
const feeds = new Collection(feedsData).map(data =>
|
feedsGroupedByChannelId,
|
||||||
new Feed(data).withChannel(channelsGroupedById)
|
blocklistRecordsGroupedByChannelId
|
||||||
)
|
}: DataProcessorData = processor.process(data)
|
||||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) =>
|
|
||||||
feed.channel ? feed.channel.id : uniqueId()
|
|
||||||
)
|
|
||||||
const blocklistContent = await dataStorage.json('blocklist.json')
|
|
||||||
const blocklist = new Collection(blocklistContent).map(data => new Blocked(data))
|
|
||||||
const blocklistGroupedByChannelId = blocklist.keyBy((blocked: Blocked) => blocked.channelId)
|
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage: streamsStorage,
|
storage: streamsStorage,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
})
|
})
|
||||||
const files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u')
|
const files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u')
|
||||||
|
@ -55,11 +51,11 @@ async function main() {
|
||||||
const buffer = new Dictionary()
|
const buffer = new Dictionary()
|
||||||
streams.forEach((stream: Stream) => {
|
streams.forEach((stream: Stream) => {
|
||||||
if (stream.channelId) {
|
if (stream.channelId) {
|
||||||
const channel = channelsGroupedById.get(stream.channelId)
|
const channel = channelsKeyById.get(stream.channelId)
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
log.add({
|
log.add({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
line: stream.line,
|
line: stream.getLine(),
|
||||||
message: `"${stream.id}" is not in the database`
|
message: `"${stream.id}" is not in the database`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -69,40 +65,43 @@ async function main() {
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
log.add({
|
log.add({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
line: stream.line,
|
line: stream.getLine(),
|
||||||
message: `"${stream.url}" is already on the playlist`
|
message: `"${stream.url}" is already on the playlist`
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
buffer.set(stream.url, true)
|
buffer.set(stream.url, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocked = stream.channel ? blocklistGroupedByChannelId.get(stream.channel.id) : false
|
const blocklistRecords = stream.channel
|
||||||
if (blocked) {
|
? new Collection(blocklistRecordsGroupedByChannelId.get(stream.channel.id))
|
||||||
if (blocked.reason === 'dmca') {
|
: new Collection()
|
||||||
|
|
||||||
|
blocklistRecords.forEach((blocklistRecord: BlocklistRecord) => {
|
||||||
|
if (blocklistRecord.reason === 'dmca') {
|
||||||
log.add({
|
log.add({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
line: stream.line,
|
line: stream.getLine(),
|
||||||
message: `"${blocked.channelId}" is on the blocklist due to claims of copyright holders (${blocked.ref})`
|
message: `"${blocklistRecord.channelId}" is on the blocklist due to claims of copyright holders (${blocklistRecord.ref})`
|
||||||
})
|
})
|
||||||
} else if (blocked.reason === 'nsfw') {
|
} else if (blocklistRecord.reason === 'nsfw') {
|
||||||
log.add({
|
log.add({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
line: stream.line,
|
line: stream.getLine(),
|
||||||
message: `"${blocked.channelId}" is on the blocklist due to NSFW content (${blocked.ref})`
|
message: `"${blocklistRecord.channelId}" is on the blocklist due to NSFW content (${blocklistRecord.ref})`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (log.notEmpty()) {
|
if (log.notEmpty()) {
|
||||||
logger.info(`\n${chalk.underline(filepath)}`)
|
console.log(`\n${chalk.underline(filepath)}`)
|
||||||
|
|
||||||
log.forEach((logItem: LogItem) => {
|
log.forEach((logItem: LogItem) => {
|
||||||
const position = logItem.line.toString().padEnd(6, ' ')
|
const position = logItem.line.toString().padEnd(6, ' ')
|
||||||
const type = logItem.type.padEnd(9, ' ')
|
const type = logItem.type.padEnd(9, ' ')
|
||||||
const status = logItem.type === 'error' ? chalk.red(type) : chalk.yellow(type)
|
const status = logItem.type === 'error' ? chalk.red(type) : chalk.yellow(type)
|
||||||
|
|
||||||
logger.info(` ${chalk.gray(position)}${status}${logItem.message}`)
|
console.log(` ${chalk.gray(position)}${status}${logItem.message}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
errors = errors.concat(log.filter((logItem: LogItem) => logItem.type === 'error'))
|
errors = errors.concat(log.filter((logItem: LogItem) => logItem.type === 'error'))
|
||||||
|
@ -110,16 +109,18 @@ async function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(
|
if (errors.count() || warnings.count()) {
|
||||||
chalk.red(
|
console.log(
|
||||||
`\n${
|
chalk.red(
|
||||||
errors.count() + warnings.count()
|
`\n${
|
||||||
} problems (${errors.count()} errors, ${warnings.count()} warnings)`
|
errors.count() + warnings.count()
|
||||||
|
} problems (${errors.count()} errors, ${warnings.count()} warnings)`
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (errors.count()) {
|
if (errors.count()) {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,44 +1,41 @@
|
||||||
|
import { DataLoader, DataProcessor, IssueLoader, PlaylistParser } from '../../core'
|
||||||
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
|
||||||
|
import { DataProcessorData } from '../../types/dataProcessor'
|
||||||
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
import { DATA_DIR, STREAMS_DIR } from '../../constants'
|
||||||
import { IssueLoader, PlaylistParser } from '../../core'
|
import { DataLoaderData } from '../../types/dataLoader'
|
||||||
import { Blocked, Channel, Issue, Stream, Feed } from '../../models'
|
import { Issue, Stream } from '../../models'
|
||||||
import { uniqueId } from 'lodash'
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
const loader = new IssueLoader()
|
const issueLoader = new IssueLoader()
|
||||||
let report = new Collection()
|
let report = new Collection()
|
||||||
|
|
||||||
logger.info('loading issues...')
|
logger.info('loading issues...')
|
||||||
const issues = await loader.load()
|
const issues = await issueLoader.load()
|
||||||
|
|
||||||
logger.info('loading data from api...')
|
logger.info('loading data from api...')
|
||||||
|
const processor = new DataProcessor()
|
||||||
const dataStorage = new Storage(DATA_DIR)
|
const dataStorage = new Storage(DATA_DIR)
|
||||||
const channelsData = await dataStorage.json('channels.json')
|
const dataLoader = new DataLoader({ storage: dataStorage })
|
||||||
const channels = new Collection(channelsData).map(data => new Channel(data))
|
const data: DataLoaderData = await dataLoader.load()
|
||||||
const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id)
|
const {
|
||||||
const feedsData = await dataStorage.json('feeds.json')
|
channelsKeyById,
|
||||||
const feeds = new Collection(feedsData).map(data =>
|
feedsGroupedByChannelId,
|
||||||
new Feed(data).withChannel(channelsGroupedById)
|
blocklistRecordsGroupedByChannelId
|
||||||
)
|
}: DataProcessorData = processor.process(data)
|
||||||
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) =>
|
|
||||||
feed.channel ? feed.channel.id : uniqueId()
|
|
||||||
)
|
|
||||||
const blocklistContent = await dataStorage.json('blocklist.json')
|
|
||||||
const blocklist = new Collection(blocklistContent).map(data => new Blocked(data))
|
|
||||||
const blocklistGroupedByChannelId = blocklist.keyBy((blocked: Blocked) => blocked.channelId)
|
|
||||||
|
|
||||||
logger.info('loading streams...')
|
logger.info('loading streams...')
|
||||||
const streamsStorage = new Storage(STREAMS_DIR)
|
const streamsStorage = new Storage(STREAMS_DIR)
|
||||||
const parser = new PlaylistParser({
|
const parser = new PlaylistParser({
|
||||||
storage: streamsStorage,
|
storage: streamsStorage,
|
||||||
channelsGroupedById,
|
channelsKeyById,
|
||||||
feedsGroupedByChannelId
|
feedsGroupedByChannelId
|
||||||
})
|
})
|
||||||
const files = await streamsStorage.list('**/*.m3u')
|
const files = await streamsStorage.list('**/*.m3u')
|
||||||
const streams = await parser.parse(files)
|
const streams = await parser.parse(files)
|
||||||
const streamsGroupedByUrl = streams.groupBy((stream: Stream) => stream.url)
|
const streamsGroupedByUrl = streams.groupBy((stream: Stream) => stream.url)
|
||||||
const streamsGroupedByChannelId = streams.groupBy((stream: Stream) => stream.channelId)
|
const streamsGroupedByChannelId = streams.groupBy((stream: Stream) => stream.channelId)
|
||||||
|
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||||
|
|
||||||
logger.info('checking broken streams reports...')
|
logger.info('checking broken streams reports...')
|
||||||
const brokenStreamReports = issues.filter(issue =>
|
const brokenStreamReports = issues.filter(issue =>
|
||||||
|
@ -94,8 +91,8 @@ async function main() {
|
||||||
|
|
||||||
if (!channelId) result.status = 'missing_id'
|
if (!channelId) result.status = 'missing_id'
|
||||||
else if (!streamUrl) result.status = 'missing_link'
|
else if (!streamUrl) result.status = 'missing_link'
|
||||||
else if (blocklistGroupedByChannelId.has(channelId)) result.status = 'blocked'
|
else if (blocklistRecordsGroupedByChannelId.has(channelId)) result.status = 'blocked'
|
||||||
else if (channelsGroupedById.missing(channelId)) result.status = 'wrong_id'
|
else if (channelsKeyById.missing(channelId)) result.status = 'wrong_id'
|
||||||
else if (streamsGroupedByUrl.has(streamUrl)) result.status = 'on_playlist'
|
else if (streamsGroupedByUrl.has(streamUrl)) result.status = 'on_playlist'
|
||||||
else if (addRequestsBuffer.has(streamUrl)) result.status = 'duplicate'
|
else if (addRequestsBuffer.has(streamUrl)) result.status = 'duplicate'
|
||||||
else result.status = 'pending'
|
else result.status = 'pending'
|
||||||
|
@ -124,7 +121,7 @@ async function main() {
|
||||||
|
|
||||||
if (!streamUrl) result.status = 'missing_link'
|
if (!streamUrl) result.status = 'missing_link'
|
||||||
else if (streamsGroupedByUrl.missing(streamUrl)) result.status = 'invalid_link'
|
else if (streamsGroupedByUrl.missing(streamUrl)) result.status = 'invalid_link'
|
||||||
else if (channelId && channelsGroupedById.missing(channelId)) result.status = 'invalid_id'
|
else if (channelId && channelsKeyById.missing(channelId)) result.status = 'invalid_id'
|
||||||
|
|
||||||
report.add(result)
|
report.add(result)
|
||||||
})
|
})
|
||||||
|
@ -147,16 +144,16 @@ async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!channelId) result.status = 'missing_id'
|
if (!channelId) result.status = 'missing_id'
|
||||||
else if (channelsGroupedById.missing(channelId)) result.status = 'invalid_id'
|
else if (channelsKeyById.missing(channelId)) result.status = 'invalid_id'
|
||||||
else if (channelSearchRequestsBuffer.has(channelId)) result.status = 'duplicate'
|
else if (channelSearchRequestsBuffer.has(streamId)) result.status = 'duplicate'
|
||||||
else if (blocklistGroupedByChannelId.has(channelId)) result.status = 'blocked'
|
else if (blocklistRecordsGroupedByChannelId.has(channelId)) result.status = 'blocked'
|
||||||
else if (streamsGroupedByChannelId.has(channelId)) result.status = 'fulfilled'
|
else if (streamsGroupedById.has(streamId)) result.status = 'fulfilled'
|
||||||
else {
|
else {
|
||||||
const channelData = channelsGroupedById.get(channelId)
|
const channelData = channelsKeyById.get(channelId)
|
||||||
if (channelData.length && channelData[0].closed) result.status = 'closed'
|
if (channelData.length && channelData[0].closed) result.status = 'closed'
|
||||||
}
|
}
|
||||||
|
|
||||||
channelSearchRequestsBuffer.set(channelId, true)
|
channelSearchRequestsBuffer.set(streamId, true)
|
||||||
|
|
||||||
report.add(result)
|
report.add(result)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,59 +1,16 @@
|
||||||
import { Logger, Storage } from '@freearhey/core'
|
import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
|
||||||
import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios'
|
|
||||||
import cliProgress, { MultiBar } from 'cli-progress'
|
|
||||||
import numeral from 'numeral'
|
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
progressBar: MultiBar
|
instance: AxiosInstance
|
||||||
client: AxiosInstance
|
|
||||||
storage: Storage
|
|
||||||
logger: Logger
|
|
||||||
|
|
||||||
constructor({ logger }: { logger: Logger }) {
|
constructor() {
|
||||||
this.logger = logger
|
this.instance = axios.create({
|
||||||
this.client = axios.create({
|
baseURL: 'https://iptv-org.github.io/api',
|
||||||
responseType: 'stream'
|
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) {
|
get(url: string, options: AxiosRequestConfig): Promise<AxiosResponse> {
|
||||||
const stream = await this.storage.createStream(`temp/data/${filename}`)
|
return this.instance.get(url, options)
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
100
scripts/core/dataLoader.ts
Normal file
100
scripts/core/dataLoader.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { ApiClient } from './apiClient'
|
||||||
|
import { Storage } from '@freearhey/core'
|
||||||
|
import cliProgress, { MultiBar } from 'cli-progress'
|
||||||
|
import numeral from 'numeral'
|
||||||
|
import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader'
|
||||||
|
|
||||||
|
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
|
||||||
|
] = 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')
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
countries,
|
||||||
|
regions,
|
||||||
|
subdivisions,
|
||||||
|
languages,
|
||||||
|
categories,
|
||||||
|
blocklist,
|
||||||
|
channels,
|
||||||
|
feeds,
|
||||||
|
timezones,
|
||||||
|
guides,
|
||||||
|
streams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
110
scripts/core/dataProcessor.ts
Normal file
110
scripts/core/dataProcessor.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { DataLoaderData } from '../types/dataLoader'
|
||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
import {
|
||||||
|
BlocklistRecord,
|
||||||
|
Subdivision,
|
||||||
|
Category,
|
||||||
|
Language,
|
||||||
|
Timezone,
|
||||||
|
Channel,
|
||||||
|
Country,
|
||||||
|
Region,
|
||||||
|
Stream,
|
||||||
|
Guide,
|
||||||
|
Feed
|
||||||
|
} from '../models'
|
||||||
|
|
||||||
|
export class DataProcessor {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
process(data: DataLoaderData) {
|
||||||
|
const categories = new Collection(data.categories).map(data => new Category(data))
|
||||||
|
const categoriesKeyById = categories.keyBy((category: Category) => category.id)
|
||||||
|
|
||||||
|
const subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data))
|
||||||
|
const subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code)
|
||||||
|
const subdivisionsGroupedByCountryCode = subdivisions.groupBy(
|
||||||
|
(subdivision: Subdivision) => subdivision.countryCode
|
||||||
|
)
|
||||||
|
|
||||||
|
let regions = new Collection(data.regions).map(data => new Region(data))
|
||||||
|
const regionsKeyByCode = regions.keyBy((region: Region) => region.code)
|
||||||
|
|
||||||
|
const blocklistRecords = new Collection(data.blocklist).map(data => new BlocklistRecord(data))
|
||||||
|
const blocklistRecordsGroupedByChannelId = blocklistRecords.groupBy(
|
||||||
|
(blocklistRecord: BlocklistRecord) => blocklistRecord.channelId
|
||||||
|
)
|
||||||
|
|
||||||
|
const streams = new Collection(data.streams).map(data => new Stream(data))
|
||||||
|
const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId())
|
||||||
|
|
||||||
|
const guides = new Collection(data.guides).map(data => new Guide(data))
|
||||||
|
const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId())
|
||||||
|
|
||||||
|
const languages = new Collection(data.languages).map(data => new Language(data))
|
||||||
|
const languagesKeyByCode = languages.keyBy((language: Language) => language.code)
|
||||||
|
|
||||||
|
const countries = new Collection(data.countries).map(data =>
|
||||||
|
new Country(data)
|
||||||
|
.withRegions(regions)
|
||||||
|
.withLanguage(languagesKeyByCode)
|
||||||
|
.withSubdivisions(subdivisionsGroupedByCountryCode)
|
||||||
|
)
|
||||||
|
const countriesKeyByCode = countries.keyBy((country: Country) => country.code)
|
||||||
|
|
||||||
|
regions = regions.map((region: Region) => region.withCountries(countriesKeyByCode))
|
||||||
|
|
||||||
|
const timezones = new Collection(data.timezones).map(data =>
|
||||||
|
new Timezone(data).withCountries(countriesKeyByCode)
|
||||||
|
)
|
||||||
|
const timezonesKeyById = timezones.keyBy((timezone: Timezone) => timezone.id)
|
||||||
|
|
||||||
|
let channels = new Collection(data.channels).map(data =>
|
||||||
|
new Channel(data)
|
||||||
|
.withCategories(categoriesKeyById)
|
||||||
|
.withCountry(countriesKeyByCode)
|
||||||
|
.withSubdivision(subdivisionsKeyByCode)
|
||||||
|
.withCategories(categoriesKeyById)
|
||||||
|
)
|
||||||
|
const channelsKeyById = channels.keyBy((channel: Channel) => channel.id)
|
||||||
|
|
||||||
|
let feeds = new Collection(data.feeds).map(data =>
|
||||||
|
new Feed(data)
|
||||||
|
.withChannel(channelsKeyById)
|
||||||
|
.withLanguages(languagesKeyByCode)
|
||||||
|
.withTimezones(timezonesKeyById)
|
||||||
|
.withBroadcastCountries(countriesKeyByCode, regionsKeyByCode, subdivisionsKeyByCode)
|
||||||
|
.withBroadcastRegions(regions)
|
||||||
|
.withBroadcastSubdivisions(subdivisionsKeyByCode)
|
||||||
|
)
|
||||||
|
const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId)
|
||||||
|
|
||||||
|
channels = channels.map((channel: Channel) => channel.withFeeds(feedsGroupedByChannelId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
blocklistRecordsGroupedByChannelId,
|
||||||
|
subdivisionsGroupedByCountryCode,
|
||||||
|
feedsGroupedByChannelId,
|
||||||
|
guidesGroupedByStreamId,
|
||||||
|
subdivisionsKeyByCode,
|
||||||
|
countriesKeyByCode,
|
||||||
|
languagesKeyByCode,
|
||||||
|
streamsGroupedById,
|
||||||
|
categoriesKeyById,
|
||||||
|
timezonesKeyById,
|
||||||
|
regionsKeyByCode,
|
||||||
|
blocklistRecords,
|
||||||
|
channelsKeyById,
|
||||||
|
subdivisions,
|
||||||
|
categories,
|
||||||
|
countries,
|
||||||
|
languages,
|
||||||
|
timezones,
|
||||||
|
channels,
|
||||||
|
regions,
|
||||||
|
streams,
|
||||||
|
guides,
|
||||||
|
feeds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,15 +16,15 @@ export class HTMLTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
let output = '<table>\n'
|
let output = '<table>\r\n'
|
||||||
|
|
||||||
output += ' <thead>\n <tr>'
|
output += ' <thead>\r\n <tr>'
|
||||||
for (const column of this.columns) {
|
for (const column of this.columns) {
|
||||||
output += `<th align="left">${column.name}</th>`
|
output += `<th align="left">${column.name}</th>`
|
||||||
}
|
}
|
||||||
output += '</tr>\n </thead>\n'
|
output += '</tr>\r\n </thead>\r\n'
|
||||||
|
|
||||||
output += ' <tbody>\n'
|
output += ' <tbody>\r\n'
|
||||||
for (const item of this.data) {
|
for (const item of this.data) {
|
||||||
output += ' <tr>'
|
output += ' <tr>'
|
||||||
let i = 0
|
let i = 0
|
||||||
|
@ -35,9 +35,9 @@ export class HTMLTable {
|
||||||
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
output += `<td${align}${nowrap}>${item[prop]}</td>`
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
output += '</tr>\n'
|
output += '</tr>\r\n'
|
||||||
}
|
}
|
||||||
output += ' </tbody>\n'
|
output += ' </tbody>\r\n'
|
||||||
|
|
||||||
output += '</table>'
|
output += '</table>'
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
export * from './playlistParser'
|
export * from './apiClient'
|
||||||
export * from './numberParser'
|
export * from './cliTable'
|
||||||
export * from './logParser'
|
export * from './dataProcessor'
|
||||||
export * from './markdown'
|
export * from './dataLoader'
|
||||||
|
export * from './htmlTable'
|
||||||
|
export * from './issueData'
|
||||||
export * from './issueLoader'
|
export * from './issueLoader'
|
||||||
export * from './issueParser'
|
export * from './issueParser'
|
||||||
export * from './htmlTable'
|
export * from './logParser'
|
||||||
export * from './apiClient'
|
export * from './markdown'
|
||||||
export * from './issueData'
|
export * from './numberParser'
|
||||||
|
export * from './playlistParser'
|
||||||
export * from './streamTester'
|
export * from './streamTester'
|
||||||
export * from './cliTable'
|
|
||||||
|
|
|
@ -5,18 +5,18 @@ import { Stream } from '../models'
|
||||||
type PlaylistPareserProps = {
|
type PlaylistPareserProps = {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
feedsGroupedByChannelId: Dictionary
|
feedsGroupedByChannelId: Dictionary
|
||||||
channelsGroupedById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlaylistParser {
|
export class PlaylistParser {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
feedsGroupedByChannelId: Dictionary
|
feedsGroupedByChannelId: Dictionary
|
||||||
channelsGroupedById: Dictionary
|
channelsKeyById: Dictionary
|
||||||
|
|
||||||
constructor({ storage, feedsGroupedByChannelId, channelsGroupedById }: PlaylistPareserProps) {
|
constructor({ storage, feedsGroupedByChannelId, channelsKeyById }: PlaylistPareserProps) {
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
this.feedsGroupedByChannelId = feedsGroupedByChannelId
|
this.feedsGroupedByChannelId = feedsGroupedByChannelId
|
||||||
this.channelsGroupedById = channelsGroupedById
|
this.channelsKeyById = channelsKeyById
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(files: string[]): Promise<Collection> {
|
async parse(files: string[]): Promise<Collection> {
|
||||||
|
@ -35,9 +35,10 @@ export class PlaylistParser {
|
||||||
const parsed: parser.Playlist = parser.parse(content)
|
const parsed: parser.Playlist = parser.parse(content)
|
||||||
|
|
||||||
const streams = new Collection(parsed.items).map((data: parser.PlaylistItem) => {
|
const streams = new Collection(parsed.items).map((data: parser.PlaylistItem) => {
|
||||||
const stream = new Stream(data)
|
const stream = new Stream()
|
||||||
|
.fromPlaylistItem(data)
|
||||||
.withFeed(this.feedsGroupedByChannelId)
|
.withFeed(this.feedsGroupedByChannelId)
|
||||||
.withChannel(this.channelsGroupedById)
|
.withChannel(this.channelsKeyById)
|
||||||
.setFilepath(filepath)
|
.setFilepath(filepath)
|
||||||
|
|
||||||
return stream
|
return stream
|
||||||
|
|
|
@ -18,8 +18,8 @@ export class StreamTester {
|
||||||
return this.checker.checkStream({
|
return this.checker.checkStream({
|
||||||
url: stream.url,
|
url: stream.url,
|
||||||
http: {
|
http: {
|
||||||
referrer: stream.getHttpReferrer(),
|
referrer: stream.getReferrer(),
|
||||||
'user-agent': stream.getHttpUserAgent()
|
'user-agent': stream.getUserAgent()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import { Generator } from './generator'
|
import { Collection, Storage, Logger, File } from '@freearhey/core'
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Stream, Category, Playlist } from '../models'
|
import { Stream, Category, Playlist } from '../models'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type CategoriesGeneratorProps = {
|
type CategoriesGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
categories: Collection
|
categories: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CategoriesGenerator implements Generator {
|
export class CategoriesGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
categories: Collection
|
categories: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, categories, logger }: CategoriesGeneratorProps) {
|
constructor({ streams, categories, logFile }: CategoriesGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.categories = categories
|
this.categories = categories
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate() {
|
async generate() {
|
||||||
|
@ -37,8 +38,8 @@ export class CategoriesGenerator implements Generator {
|
||||||
const playlist = new Playlist(categoryStreams, { public: true })
|
const playlist = new Playlist(categoryStreams, { public: true })
|
||||||
const filepath = `categories/${category.id}.m3u`
|
const filepath = `categories/${category.id}.m3u`
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() })
|
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -46,8 +47,8 @@ export class CategoriesGenerator implements Generator {
|
||||||
const playlist = new Playlist(undefinedStreams, { public: true })
|
const playlist = new Playlist(undefinedStreams, { public: true })
|
||||||
const filepath = 'categories/undefined.m3u'
|
const filepath = 'categories/undefined.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() })
|
JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import { Generator } from './generator'
|
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Country, Subdivision, Stream, Playlist } from '../models'
|
import { Country, Subdivision, Stream, Playlist } from '../models'
|
||||||
|
import { Collection, Storage, File } from '@freearhey/core'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type CountriesGeneratorProps = {
|
type CountriesGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
countries: Collection
|
countries: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CountriesGenerator implements Generator {
|
export class CountriesGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
countries: Collection
|
countries: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, countries, logger }: CountriesGeneratorProps) {
|
constructor({ streams, countries, logFile }: CountriesGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.countries = countries
|
this.countries = countries
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -36,8 +37,8 @@ export class CountriesGenerator implements Generator {
|
||||||
const playlist = new Playlist(countryStreams, { public: true })
|
const playlist = new Playlist(countryStreams, { public: true })
|
||||||
const filepath = `countries/${country.code.toLowerCase()}.m3u`
|
const filepath = `countries/${country.code.toLowerCase()}.m3u`
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() })
|
JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL
|
||||||
)
|
)
|
||||||
|
|
||||||
country.getSubdivisions().forEach(async (subdivision: Subdivision) => {
|
country.getSubdivisions().forEach(async (subdivision: Subdivision) => {
|
||||||
|
@ -50,8 +51,8 @@ export class CountriesGenerator implements Generator {
|
||||||
const playlist = new Playlist(subdivisionStreams, { public: true })
|
const playlist = new Playlist(subdivisionStreams, { public: true })
|
||||||
const filepath = `subdivisions/${subdivision.code.toLowerCase()}.m3u`
|
const filepath = `subdivisions/${subdivision.code.toLowerCase()}.m3u`
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({ type: 'subdivision', filepath, count: playlist.streams.count() })
|
JSON.stringify({ type: 'subdivision', filepath, count: playlist.streams.count() }) + EOL
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -60,12 +61,12 @@ export class CountriesGenerator implements Generator {
|
||||||
const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
|
const undefinedPlaylist = new Playlist(undefinedStreams, { public: true })
|
||||||
const undefinedFilepath = 'countries/undefined.m3u'
|
const undefinedFilepath = 'countries/undefined.m3u'
|
||||||
await this.storage.save(undefinedFilepath, undefinedPlaylist.toString())
|
await this.storage.save(undefinedFilepath, undefinedPlaylist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'country',
|
type: 'country',
|
||||||
filepath: undefinedFilepath,
|
filepath: undefinedFilepath,
|
||||||
count: undefinedPlaylist.streams.count()
|
count: undefinedPlaylist.streams.count()
|
||||||
})
|
}) + EOL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import { Generator } from './generator'
|
import { Collection, Storage, File } from '@freearhey/core'
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Stream, Playlist, Category } from '../models'
|
import { Stream, Playlist, Category } from '../models'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type IndexCategoryGeneratorProps = {
|
type IndexCategoryGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexCategoryGenerator implements Generator {
|
export class IndexCategoryGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, logger }: IndexCategoryGeneratorProps) {
|
constructor({ streams, logFile }: IndexCategoryGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -48,6 +49,8 @@ export class IndexCategoryGenerator implements Generator {
|
||||||
const playlist = new Playlist(groupedStreams, { public: true })
|
const playlist = new Playlist(groupedStreams, { public: true })
|
||||||
const filepath = 'index.category.m3u'
|
const filepath = 'index.category.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }))
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import { Generator } from './generator'
|
import { Collection, Storage, File } from '@freearhey/core'
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Stream, Playlist, Country } from '../models'
|
import { Stream, Playlist, Country } from '../models'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type IndexCountryGeneratorProps = {
|
type IndexCountryGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexCountryGenerator implements Generator {
|
export class IndexCountryGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, logger }: IndexCountryGeneratorProps) {
|
constructor({ streams, logFile }: IndexCountryGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -56,6 +57,8 @@ export class IndexCountryGenerator implements Generator {
|
||||||
const playlist = new Playlist(groupedStreams, { public: true })
|
const playlist = new Playlist(groupedStreams, { public: true })
|
||||||
const filepath = 'index.country.m3u'
|
const filepath = 'index.country.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }))
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import { Collection, Logger, Storage } from '@freearhey/core'
|
import { Collection, File, Storage } from '@freearhey/core'
|
||||||
import { Stream, Playlist } from '../models'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type IndexGeneratorProps = {
|
type IndexGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexGenerator implements Generator {
|
export class IndexGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, logger }: IndexGeneratorProps) {
|
constructor({ streams, logFile }: IndexGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -27,6 +28,8 @@ export class IndexGenerator implements Generator {
|
||||||
const playlist = new Playlist(sfwStreams, { public: true })
|
const playlist = new Playlist(sfwStreams, { public: true })
|
||||||
const filepath = 'index.m3u'
|
const filepath = 'index.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }))
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import { Generator } from './generator'
|
import { Collection, Storage, File } from '@freearhey/core'
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Stream, Playlist, Language } from '../models'
|
import { Stream, Playlist, Language } from '../models'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type IndexLanguageGeneratorProps = {
|
type IndexLanguageGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexLanguageGenerator implements Generator {
|
export class IndexLanguageGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, logger }: IndexLanguageGeneratorProps) {
|
constructor({ streams, logFile }: IndexLanguageGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -47,6 +48,8 @@ export class IndexLanguageGenerator implements Generator {
|
||||||
const playlist = new Playlist(groupedStreams, { public: true })
|
const playlist = new Playlist(groupedStreams, { public: true })
|
||||||
const filepath = 'index.language.m3u'
|
const filepath = 'index.language.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }))
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import { Collection, Logger, Storage } from '@freearhey/core'
|
import { Collection, File, Storage } from '@freearhey/core'
|
||||||
import { Stream, Playlist } from '../models'
|
import { Stream, Playlist } from '../models'
|
||||||
import { Generator } from './generator'
|
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type IndexNsfwGeneratorProps = {
|
type IndexNsfwGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexNsfwGenerator implements Generator {
|
export class IndexNsfwGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, logger }: IndexNsfwGeneratorProps) {
|
constructor({ streams, logFile }: IndexNsfwGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -25,6 +26,8 @@ export class IndexNsfwGenerator implements Generator {
|
||||||
const playlist = new Playlist(allStreams, { public: true })
|
const playlist = new Playlist(allStreams, { public: true })
|
||||||
const filepath = 'index.nsfw.m3u'
|
const filepath = 'index.nsfw.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }))
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import { Generator } from './generator'
|
import { Collection, Storage, File } from '@freearhey/core'
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Stream, Playlist, Region } from '../models'
|
import { Stream, Playlist, Region } from '../models'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type IndexRegionGeneratorProps = {
|
type IndexRegionGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
regions: Collection
|
regions: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexRegionGenerator implements Generator {
|
export class IndexRegionGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
regions: Collection
|
regions: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, regions, logger }: IndexRegionGeneratorProps) {
|
constructor({ streams, regions, logFile }: IndexRegionGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.regions = regions
|
this.regions = regions
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -58,6 +59,8 @@ export class IndexRegionGenerator implements Generator {
|
||||||
const playlist = new Playlist(groupedStreams, { public: true })
|
const playlist = new Playlist(groupedStreams, { public: true })
|
||||||
const filepath = 'index.region.m3u'
|
const filepath = 'index.region.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }))
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import { Generator } from './generator'
|
import { Collection, Storage, File } from '@freearhey/core'
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Playlist, Language, Stream } from '../models'
|
import { Playlist, Language, Stream } from '../models'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type LanguagesGeneratorProps = { streams: Collection; logger: Logger }
|
type LanguagesGeneratorProps = { streams: Collection; logFile: File }
|
||||||
|
|
||||||
export class LanguagesGenerator implements Generator {
|
export class LanguagesGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, logger }: LanguagesGeneratorProps) {
|
constructor({ streams, logFile }: LanguagesGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -38,8 +39,8 @@ export class LanguagesGenerator implements Generator {
|
||||||
const playlist = new Playlist(languageStreams, { public: true })
|
const playlist = new Playlist(languageStreams, { public: true })
|
||||||
const filepath = `languages/${language.code}.m3u`
|
const filepath = `languages/${language.code}.m3u`
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() })
|
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -50,8 +51,8 @@ export class LanguagesGenerator implements Generator {
|
||||||
const playlist = new Playlist(undefinedStreams, { public: true })
|
const playlist = new Playlist(undefinedStreams, { public: true })
|
||||||
const filepath = 'languages/undefined.m3u'
|
const filepath = 'languages/undefined.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() })
|
JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import { Generator } from './generator'
|
import { Collection, Storage, File } from '@freearhey/core'
|
||||||
import { Collection, Storage, Logger } from '@freearhey/core'
|
|
||||||
import { Playlist, Region, Stream } from '../models'
|
import { Playlist, Region, Stream } from '../models'
|
||||||
import { PUBLIC_DIR } from '../constants'
|
import { PUBLIC_DIR } from '../constants'
|
||||||
|
import { Generator } from './generator'
|
||||||
|
import { EOL } from 'node:os'
|
||||||
|
|
||||||
type RegionsGeneratorProps = {
|
type RegionsGeneratorProps = {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
regions: Collection
|
regions: Collection
|
||||||
logger: Logger
|
logFile: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RegionsGenerator implements Generator {
|
export class RegionsGenerator implements Generator {
|
||||||
streams: Collection
|
streams: Collection
|
||||||
regions: Collection
|
regions: Collection
|
||||||
storage: Storage
|
storage: Storage
|
||||||
logger: Logger
|
logFile: File
|
||||||
|
|
||||||
constructor({ streams, regions, logger }: RegionsGeneratorProps) {
|
constructor({ streams, regions, logFile }: RegionsGeneratorProps) {
|
||||||
this.streams = streams
|
this.streams = streams
|
||||||
this.regions = regions
|
this.regions = regions
|
||||||
this.storage = new Storage(PUBLIC_DIR)
|
this.storage = new Storage(PUBLIC_DIR)
|
||||||
this.logger = logger
|
this.logFile = logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<void> {
|
async generate(): Promise<void> {
|
||||||
|
@ -35,8 +36,8 @@ export class RegionsGenerator implements Generator {
|
||||||
const playlist = new Playlist(regionStreams, { public: true })
|
const playlist = new Playlist(regionStreams, { public: true })
|
||||||
const filepath = `regions/${region.code.toLowerCase()}.m3u`
|
const filepath = `regions/${region.code.toLowerCase()}.m3u`
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() })
|
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -44,18 +45,20 @@ export class RegionsGenerator implements Generator {
|
||||||
const internationalPlaylist = new Playlist(internationalStreams, { public: true })
|
const internationalPlaylist = new Playlist(internationalStreams, { public: true })
|
||||||
const internationalFilepath = 'regions/int.m3u'
|
const internationalFilepath = 'regions/int.m3u'
|
||||||
await this.storage.save(internationalFilepath, internationalPlaylist.toString())
|
await this.storage.save(internationalFilepath, internationalPlaylist.toString())
|
||||||
this.logger.info(
|
this.logFile.append(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'region',
|
type: 'region',
|
||||||
filepath: internationalFilepath,
|
filepath: internationalFilepath,
|
||||||
count: internationalPlaylist.streams.count()
|
count: internationalPlaylist.streams.count()
|
||||||
})
|
}) + EOL
|
||||||
)
|
)
|
||||||
|
|
||||||
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
|
const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea())
|
||||||
const playlist = new Playlist(undefinedStreams, { public: true })
|
const playlist = new Playlist(undefinedStreams, { public: true })
|
||||||
const filepath = 'regions/undefined.m3u'
|
const filepath = 'regions/undefined.m3u'
|
||||||
await this.storage.save(filepath, playlist.toString())
|
await this.storage.save(filepath, playlist.toString())
|
||||||
this.logger.info(JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }))
|
this.logFile.append(
|
||||||
|
JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
type BlockedProps = {
|
|
||||||
channel: string
|
|
||||||
reason: string
|
|
||||||
ref: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Blocked {
|
|
||||||
channelId: string
|
|
||||||
reason: string
|
|
||||||
ref: string
|
|
||||||
|
|
||||||
constructor(data: BlockedProps) {
|
|
||||||
this.channelId = data.channel
|
|
||||||
this.reason = data.reason
|
|
||||||
this.ref = data.ref
|
|
||||||
}
|
|
||||||
}
|
|
15
scripts/models/blocklistRecord.ts
Normal file
15
scripts/models/blocklistRecord.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import type { BlocklistRecordData } from '../types/blocklistRecord'
|
||||||
|
|
||||||
|
export class BlocklistRecord {
|
||||||
|
channelId: string
|
||||||
|
reason: string
|
||||||
|
ref: string
|
||||||
|
|
||||||
|
constructor(data?: BlocklistRecordData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
this.channelId = data.channel
|
||||||
|
this.reason = data.reason
|
||||||
|
this.ref = data.ref
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,4 @@
|
||||||
type CategoryData = {
|
import type { CategoryData, CategorySerializedData } from '../types/category'
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Category {
|
export class Category {
|
||||||
id: string
|
id: string
|
||||||
|
@ -11,4 +8,11 @@ export class Category {
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serialize(): CategorySerializedData {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,6 @@
|
||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Category, Country, Subdivision } from './index'
|
import { Category, Country, Feed, Guide, Stream, Subdivision } from './index'
|
||||||
|
import type { ChannelData, ChannelSearchableData, ChannelSerializedData } from '../types/channel'
|
||||||
type 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
|
|
||||||
logo: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Channel {
|
export class Channel {
|
||||||
id: string
|
id: string
|
||||||
|
@ -31,15 +14,18 @@ export class Channel {
|
||||||
subdivision?: Subdivision
|
subdivision?: Subdivision
|
||||||
cityName?: string
|
cityName?: string
|
||||||
categoryIds: Collection
|
categoryIds: Collection
|
||||||
categories?: Collection
|
categories: Collection = new Collection()
|
||||||
isNSFW: boolean
|
isNSFW: boolean
|
||||||
launched?: string
|
launched?: string
|
||||||
closed?: string
|
closed?: string
|
||||||
replacedBy?: string
|
replacedBy?: string
|
||||||
website?: string
|
website?: string
|
||||||
logo: string
|
logo: string
|
||||||
|
feeds?: Collection
|
||||||
|
|
||||||
|
constructor(data?: ChannelData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
constructor(data: ChannelData) {
|
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.altNames = new Collection(data.alt_names)
|
this.altNames = new Collection(data.alt_names)
|
||||||
|
@ -57,28 +43,34 @@ export class Channel {
|
||||||
this.logo = data.logo
|
this.logo = data.logo
|
||||||
}
|
}
|
||||||
|
|
||||||
withSubdivision(subdivisionsGroupedByCode: Dictionary): this {
|
withSubdivision(subdivisionsKeyByCode: Dictionary): this {
|
||||||
if (!this.subdivisionCode) return this
|
if (!this.subdivisionCode) return this
|
||||||
|
|
||||||
this.subdivision = subdivisionsGroupedByCode.get(this.subdivisionCode)
|
this.subdivision = subdivisionsKeyByCode.get(this.subdivisionCode)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withCountry(countriesGroupedByCode: Dictionary): this {
|
withCountry(countriesKeyByCode: Dictionary): this {
|
||||||
this.country = countriesGroupedByCode.get(this.countryCode)
|
this.country = countriesKeyByCode.get(this.countryCode)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withCategories(groupedCategories: Dictionary): this {
|
withCategories(categoriesKeyById: Dictionary): this {
|
||||||
this.categories = this.categoryIds
|
this.categories = this.categoryIds
|
||||||
.map((id: string) => groupedCategories.get(id))
|
.map((id: string) => categoriesKeyById.get(id))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withFeeds(feedsGroupedByChannelId: Dictionary): this {
|
||||||
|
this.feeds = new Collection(feedsGroupedByChannelId.get(this.id))
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
getCountry(): Country | undefined {
|
getCountry(): Country | undefined {
|
||||||
return this.country
|
return this.country
|
||||||
}
|
}
|
||||||
|
@ -102,7 +94,106 @@ export class Channel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFeeds(): Collection {
|
||||||
|
if (!this.feeds) return new Collection()
|
||||||
|
|
||||||
|
return this.feeds
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuides(): Collection {
|
||||||
|
let guides = new Collection()
|
||||||
|
|
||||||
|
this.getFeeds().forEach((feed: Feed) => {
|
||||||
|
guides = guides.concat(feed.getGuides())
|
||||||
|
})
|
||||||
|
|
||||||
|
return guides
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuideNames(): Collection {
|
||||||
|
return this.getGuides()
|
||||||
|
.map((guide: Guide) => guide.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()
|
||||||
|
}
|
||||||
|
|
||||||
isSFW(): boolean {
|
isSFW(): boolean {
|
||||||
return this.isNSFW === false
|
return this.isNSFW === false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSearchable(): ChannelSearchableData {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
altNames: this.altNames.all(),
|
||||||
|
guideNames: this.getGuideNames().all(),
|
||||||
|
streamNames: this.getStreamNames().all(),
|
||||||
|
feedFullNames: this.getFeedFullNames().all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): ChannelSerializedData {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
altNames: this.altNames.all(),
|
||||||
|
network: this.network,
|
||||||
|
owners: this.owners.all(),
|
||||||
|
countryCode: this.countryCode,
|
||||||
|
country: this.country ? this.country.serialize() : undefined,
|
||||||
|
subdivisionCode: this.subdivisionCode,
|
||||||
|
subdivision: this.subdivision ? this.subdivision.serialize() : undefined,
|
||||||
|
cityName: this.cityName,
|
||||||
|
categoryIds: this.categoryIds.all(),
|
||||||
|
categories: this.categories.map((category: Category) => category.serialize()).all(),
|
||||||
|
isNSFW: this.isNSFW,
|
||||||
|
launched: this.launched,
|
||||||
|
closed: this.closed,
|
||||||
|
replacedBy: this.replacedBy,
|
||||||
|
website: this.website,
|
||||||
|
logo: this.logo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: ChannelSerializedData): this {
|
||||||
|
this.id = data.id
|
||||||
|
this.name = data.name
|
||||||
|
this.altNames = new Collection(data.altNames)
|
||||||
|
this.network = data.network
|
||||||
|
this.owners = new Collection(data.owners)
|
||||||
|
this.countryCode = data.countryCode
|
||||||
|
this.country = data.country ? new Country().deserialize(data.country) : undefined
|
||||||
|
this.subdivisionCode = data.subdivisionCode
|
||||||
|
this.cityName = data.cityName
|
||||||
|
this.categoryIds = new Collection(data.categoryIds)
|
||||||
|
this.isNSFW = data.isNSFW
|
||||||
|
this.launched = data.launched
|
||||||
|
this.closed = data.closed
|
||||||
|
this.replacedBy = data.replacedBy
|
||||||
|
this.website = data.website
|
||||||
|
this.logo = data.logo
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Region, Language } from '.'
|
import { Region, Language, Subdivision } from '.'
|
||||||
|
import type { CountryData, CountrySerializedData } from '../types/country'
|
||||||
type CountryData = {
|
import { SubdivisionSerializedData } from '../types/subdivision'
|
||||||
code: string
|
import { RegionSerializedData } from '../types/region'
|
||||||
name: string
|
|
||||||
lang: string
|
|
||||||
flag: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Country {
|
export class Country {
|
||||||
code: string
|
code: string
|
||||||
|
@ -17,7 +13,9 @@ export class Country {
|
||||||
subdivisions?: Collection
|
subdivisions?: Collection
|
||||||
regions?: Collection
|
regions?: Collection
|
||||||
|
|
||||||
constructor(data: CountryData) {
|
constructor(data?: CountryData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
this.code = data.code
|
this.code = data.code
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.flag = data.flag
|
this.flag = data.flag
|
||||||
|
@ -38,8 +36,8 @@ export class Country {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withLanguage(languagesGroupedByCode: Dictionary): this {
|
withLanguage(languagesKeyByCode: Dictionary): this {
|
||||||
this.language = languagesGroupedByCode.get(this.languageCode)
|
this.language = languagesKeyByCode.get(this.languageCode)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -55,4 +53,34 @@ export class Country {
|
||||||
getSubdivisions(): Collection {
|
getSubdivisions(): Collection {
|
||||||
return this.subdivisions || new Collection()
|
return this.subdivisions || new Collection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serialize(): CountrySerializedData {
|
||||||
|
return {
|
||||||
|
code: this.code,
|
||||||
|
name: this.name,
|
||||||
|
flag: this.flag,
|
||||||
|
languageCode: this.languageCode,
|
||||||
|
language: this.language ? this.language.serialize() : null,
|
||||||
|
subdivisions: this.subdivisions
|
||||||
|
? this.subdivisions.map((subdivision: Subdivision) => subdivision.serialize()).all()
|
||||||
|
: [],
|
||||||
|
regions: this.regions ? this.regions.map((region: Region) => region.serialize()).all() : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: CountrySerializedData): this {
|
||||||
|
this.code = data.code
|
||||||
|
this.name = data.name
|
||||||
|
this.flag = data.flag
|
||||||
|
this.languageCode = data.languageCode
|
||||||
|
this.language = data.language ? new Language().deserialize(data.language) : undefined
|
||||||
|
this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) =>
|
||||||
|
new Subdivision().deserialize(data)
|
||||||
|
)
|
||||||
|
this.regions = new Collection(data.regions).map((data: RegionSerializedData) =>
|
||||||
|
new Region().deserialize(data)
|
||||||
|
)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,6 @@
|
||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Country, Language, Region, Channel, Subdivision } from './index'
|
import { Country, Language, Region, Channel, Subdivision } from './index'
|
||||||
|
import type { FeedData } from '../types/feed'
|
||||||
type FeedData = {
|
|
||||||
channel: string
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
is_main: boolean
|
|
||||||
broadcast_area: Collection
|
|
||||||
languages: Collection
|
|
||||||
timezones: Collection
|
|
||||||
video_format: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Feed {
|
export class Feed {
|
||||||
channelId: string
|
channelId: string
|
||||||
|
@ -30,6 +20,8 @@ export class Feed {
|
||||||
timezoneIds: Collection
|
timezoneIds: Collection
|
||||||
timezones?: Collection
|
timezones?: Collection
|
||||||
videoFormat: string
|
videoFormat: string
|
||||||
|
guides?: Collection
|
||||||
|
streams?: Collection
|
||||||
|
|
||||||
constructor(data: FeedData) {
|
constructor(data: FeedData) {
|
||||||
this.channelId = data.channel
|
this.channelId = data.channel
|
||||||
|
@ -61,40 +53,58 @@ export class Feed {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
withChannel(channelsGroupedById: Dictionary): this {
|
withChannel(channelsKeyById: Dictionary): this {
|
||||||
this.channel = channelsGroupedById.get(this.channelId)
|
this.channel = channelsKeyById.get(this.channelId)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withLanguages(languagesGroupedByCode: Dictionary): 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
|
||||||
|
}
|
||||||
|
|
||||||
|
withGuides(guidesGroupedByStreamId: Dictionary): this {
|
||||||
|
this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`))
|
||||||
|
|
||||||
|
if (this.isMain) {
|
||||||
|
this.guides = this.guides.concat(new Collection(guidesGroupedByStreamId.get(this.channelId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withLanguages(languagesKeyByCode: Dictionary): this {
|
||||||
this.languages = this.languageCodes
|
this.languages = this.languageCodes
|
||||||
.map((code: string) => languagesGroupedByCode.get(code))
|
.map((code: string) => languagesKeyByCode.get(code))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withTimezones(timezonesGroupedById: Dictionary): this {
|
withTimezones(timezonesKeyById: Dictionary): this {
|
||||||
this.timezones = this.timezoneIds
|
this.timezones = this.timezoneIds.map((id: string) => timezonesKeyById.get(id)).filter(Boolean)
|
||||||
.map((id: string) => timezonesGroupedById.get(id))
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withBroadcastSubdivisions(subdivisionsGroupedByCode: Dictionary): this {
|
withBroadcastSubdivisions(subdivisionsKeyByCode: Dictionary): this {
|
||||||
this.broadcastSubdivisions = this.broadcastSubdivisionCodes.map((code: string) =>
|
this.broadcastSubdivisions = this.broadcastSubdivisionCodes.map((code: string) =>
|
||||||
subdivisionsGroupedByCode.get(code)
|
subdivisionsKeyByCode.get(code)
|
||||||
)
|
)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withBroadcastCountries(
|
withBroadcastCountries(
|
||||||
countriesGroupedByCode: Dictionary,
|
countriesKeyByCode: Dictionary,
|
||||||
regionsGroupedByCode: Dictionary,
|
regionsKeyByCode: Dictionary,
|
||||||
subdivisionsGroupedByCode: Dictionary
|
subdivisionsKeyByCode: Dictionary
|
||||||
): this {
|
): this {
|
||||||
let broadcastCountries = new Collection()
|
let broadcastCountries = new Collection()
|
||||||
|
|
||||||
|
@ -104,22 +114,22 @@ export class Feed {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.broadcastCountryCodes.forEach((code: string) => {
|
this.broadcastCountryCodes.forEach((code: string) => {
|
||||||
broadcastCountries.add(countriesGroupedByCode.get(code))
|
broadcastCountries.add(countriesKeyByCode.get(code))
|
||||||
})
|
})
|
||||||
|
|
||||||
this.broadcastRegionCodes.forEach((code: string) => {
|
this.broadcastRegionCodes.forEach((code: string) => {
|
||||||
const region: Region = regionsGroupedByCode.get(code)
|
const region: Region = regionsKeyByCode.get(code)
|
||||||
if (region) {
|
if (region) {
|
||||||
region.countryCodes.forEach((countryCode: string) => {
|
region.countryCodes.forEach((countryCode: string) => {
|
||||||
broadcastCountries.add(countriesGroupedByCode.get(countryCode))
|
broadcastCountries.add(countriesKeyByCode.get(countryCode))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.broadcastSubdivisionCodes.forEach((code: string) => {
|
this.broadcastSubdivisionCodes.forEach((code: string) => {
|
||||||
const subdivision: Subdivision = subdivisionsGroupedByCode.get(code)
|
const subdivision: Subdivision = subdivisionsKeyByCode.get(code)
|
||||||
if (subdivision) {
|
if (subdivision) {
|
||||||
broadcastCountries.add(countriesGroupedByCode.get(subdivision.countryCode))
|
broadcastCountries.add(countriesKeyByCode.get(subdivision.countryCode))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -134,8 +144,8 @@ export class Feed {
|
||||||
|
|
||||||
this.broadcastRegions = regions.filter((region: Region) => {
|
this.broadcastRegions = regions.filter((region: Region) => {
|
||||||
if (region.code === 'INT') return false
|
if (region.code === 'INT') return false
|
||||||
|
const intersected = region.countryCodes.intersects(countriesCodes)
|
||||||
return region.countryCodes.intersects(countriesCodes)
|
return intersected.notEmpty()
|
||||||
})
|
})
|
||||||
|
|
||||||
return this
|
return this
|
||||||
|
@ -197,4 +207,22 @@ export class Feed {
|
||||||
|
|
||||||
return this.getBroadcastRegions().includes((_region: Region) => _region.code === region.code)
|
return this.getBroadcastRegions().includes((_region: Region) => _region.code === region.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGuides(): Collection {
|
||||||
|
if (!this.guides) return new Collection()
|
||||||
|
|
||||||
|
return this.guides
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreams(): Collection {
|
||||||
|
if (!this.streams) return new Collection()
|
||||||
|
|
||||||
|
return this.streams
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullName(): string {
|
||||||
|
if (!this.channel) return ''
|
||||||
|
|
||||||
|
return `${this.channel.name} ${this.name}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
54
scripts/models/guide.ts
Normal file
54
scripts/models/guide.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import type { GuideData, GuideSerializedData } from '../types/guide'
|
||||||
|
|
||||||
|
export class Guide {
|
||||||
|
channelId?: string
|
||||||
|
feedId?: string
|
||||||
|
siteDomain: string
|
||||||
|
siteId: string
|
||||||
|
siteName: string
|
||||||
|
languageCode: string
|
||||||
|
|
||||||
|
constructor(data?: GuideData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
this.channelId = data.channel
|
||||||
|
this.feedId = data.feed
|
||||||
|
this.siteDomain = data.site
|
||||||
|
this.siteId = data.site_id
|
||||||
|
this.siteName = data.site_name
|
||||||
|
this.languageCode = data.lang
|
||||||
|
}
|
||||||
|
|
||||||
|
getUUID(): string {
|
||||||
|
return this.getStreamId() + this.siteId
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreamId(): string | undefined {
|
||||||
|
if (!this.channelId) return undefined
|
||||||
|
if (!this.feedId) return this.channelId
|
||||||
|
|
||||||
|
return `${this.channelId}@${this.feedId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): GuideSerializedData {
|
||||||
|
return {
|
||||||
|
channelId: this.channelId,
|
||||||
|
feedId: this.feedId,
|
||||||
|
siteDomain: this.siteDomain,
|
||||||
|
siteId: this.siteId,
|
||||||
|
siteName: this.siteName,
|
||||||
|
languageCode: this.languageCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: GuideSerializedData): this {
|
||||||
|
this.channelId = data.channelId
|
||||||
|
this.feedId = data.feedId
|
||||||
|
this.siteDomain = data.siteDomain
|
||||||
|
this.siteId = data.siteId
|
||||||
|
this.siteName = data.siteName
|
||||||
|
this.languageCode = data.languageCode
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
export * from './issue'
|
export * from './blocklistRecord'
|
||||||
export * from './playlist'
|
export * from './broadcastArea'
|
||||||
export * from './blocked'
|
|
||||||
export * from './stream'
|
|
||||||
export * from './category'
|
export * from './category'
|
||||||
export * from './channel'
|
export * from './channel'
|
||||||
export * from './language'
|
|
||||||
export * from './country'
|
export * from './country'
|
||||||
export * from './region'
|
|
||||||
export * from './subdivision'
|
|
||||||
export * from './feed'
|
export * from './feed'
|
||||||
export * from './broadcastArea'
|
export * from './guide'
|
||||||
|
export * from './issue'
|
||||||
|
export * from './language'
|
||||||
|
export * from './playlist'
|
||||||
|
export * from './region'
|
||||||
|
export * from './stream'
|
||||||
|
export * from './subdivision'
|
||||||
export * from './timezone'
|
export * from './timezone'
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
type LanguageData = {
|
import type { LanguageData, LanguageSerializedData } from '../types/language'
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Language {
|
export class Language {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
constructor(data: LanguageData) {
|
constructor(data?: LanguageData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
this.code = data.code
|
this.code = data.code
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serialize(): LanguageSerializedData {
|
||||||
|
return {
|
||||||
|
code: this.code,
|
||||||
|
name: this.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: LanguageSerializedData): this {
|
||||||
|
this.code = data.code
|
||||||
|
this.name = data.name
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,10 @@ export class Playlist {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
let output = '#EXTM3U\n'
|
let output = '#EXTM3U\r\n'
|
||||||
|
|
||||||
this.streams.forEach((stream: Stream) => {
|
this.streams.forEach((stream: Stream) => {
|
||||||
output += stream.toString(this.options) + '\n'
|
output += stream.toString(this.options) + '\r\n'
|
||||||
})
|
})
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
|
@ -1,27 +1,26 @@
|
||||||
import { Collection, Dictionary } from '@freearhey/core'
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
import { Subdivision } from '.'
|
import { Country, Subdivision } from '.'
|
||||||
|
import type { RegionData, RegionSerializedData } from '../types/region'
|
||||||
type RegionData = {
|
import { CountrySerializedData } from '../types/country'
|
||||||
code: string
|
import { SubdivisionSerializedData } from '../types/subdivision'
|
||||||
name: string
|
|
||||||
countries: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Region {
|
export class Region {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
countryCodes: Collection
|
countryCodes: Collection
|
||||||
countries?: Collection
|
countries: Collection = new Collection()
|
||||||
subdivisions?: Collection
|
subdivisions: Collection = new Collection()
|
||||||
|
|
||||||
|
constructor(data?: RegionData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
constructor(data: RegionData) {
|
|
||||||
this.code = data.code
|
this.code = data.code
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.countryCodes = new Collection(data.countries)
|
this.countryCodes = new Collection(data.countries)
|
||||||
}
|
}
|
||||||
|
|
||||||
withCountries(countriesGroupedByCode: Dictionary): this {
|
withCountries(countriesKeyByCode: Dictionary): this {
|
||||||
this.countries = this.countryCodes.map((code: string) => countriesGroupedByCode.get(code))
|
this.countries = this.countryCodes.map((code: string) => countriesKeyByCode.get(code))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -35,11 +34,11 @@ export class Region {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubdivisions(): Collection {
|
getSubdivisions(): Collection {
|
||||||
return this.subdivisions || new Collection()
|
return this.subdivisions
|
||||||
}
|
}
|
||||||
|
|
||||||
getCountries(): Collection {
|
getCountries(): Collection {
|
||||||
return this.countries || new Collection()
|
return this.countries
|
||||||
}
|
}
|
||||||
|
|
||||||
includesCountryCode(code: string): boolean {
|
includesCountryCode(code: string): boolean {
|
||||||
|
@ -49,4 +48,30 @@ export class Region {
|
||||||
isWorldwide(): boolean {
|
isWorldwide(): boolean {
|
||||||
return this.code === 'INT'
|
return this.code === 'INT'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serialize(): RegionSerializedData {
|
||||||
|
return {
|
||||||
|
code: this.code,
|
||||||
|
name: this.name,
|
||||||
|
countryCodes: this.countryCodes.all(),
|
||||||
|
countries: this.countries.map((country: Country) => country.serialize()).all(),
|
||||||
|
subdivisions: this.subdivisions
|
||||||
|
.map((subdivision: Subdivision) => subdivision.serialize())
|
||||||
|
.all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: RegionSerializedData): this {
|
||||||
|
this.code = data.code
|
||||||
|
this.name = data.name
|
||||||
|
this.countryCodes = new Collection(data.countryCodes)
|
||||||
|
this.countries = new Collection(data.countries).map((data: CountrySerializedData) =>
|
||||||
|
new Country().deserialize(data)
|
||||||
|
)
|
||||||
|
this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) =>
|
||||||
|
new Subdivision().deserialize(data)
|
||||||
|
)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,45 @@
|
||||||
import { URL, Collection, Dictionary } from '@freearhey/core'
|
|
||||||
import { Feed, Channel, Category, Region, Subdivision, Country, Language } from './index'
|
import { Feed, Channel, Category, Region, Subdivision, Country, Language } from './index'
|
||||||
|
import { URL, Collection, Dictionary } from '@freearhey/core'
|
||||||
|
import type { StreamData } from '../types/stream'
|
||||||
import parser from 'iptv-playlist-parser'
|
import parser from 'iptv-playlist-parser'
|
||||||
|
|
||||||
export class Stream {
|
export class Stream {
|
||||||
name: string
|
name?: string
|
||||||
url: string
|
url: string
|
||||||
id?: string
|
id?: string
|
||||||
groupTitle: string
|
|
||||||
channelId?: string
|
channelId?: string
|
||||||
channel?: Channel
|
channel?: Channel
|
||||||
feedId?: string
|
feedId?: string
|
||||||
feed?: Feed
|
feed?: Feed
|
||||||
filepath?: string
|
filepath?: string
|
||||||
line: number
|
line?: number
|
||||||
label?: string
|
label?: string
|
||||||
verticalResolution?: number
|
verticalResolution?: number
|
||||||
isInterlaced?: boolean
|
isInterlaced?: boolean
|
||||||
httpReferrer?: string
|
referrer?: string
|
||||||
httpUserAgent?: string
|
userAgent?: string
|
||||||
|
groupTitle: string = 'Undefined'
|
||||||
removed: boolean = false
|
removed: boolean = false
|
||||||
|
|
||||||
constructor(data: parser.PlaylistItem) {
|
constructor(data?: StreamData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fromPlaylistItem(data: parser.PlaylistItem): this {
|
||||||
if (!data.name) throw new Error('"name" property is required')
|
if (!data.name) throw new Error('"name" property is required')
|
||||||
if (!data.url) throw new Error('"url" property is required')
|
if (!data.url) throw new Error('"url" property is required')
|
||||||
|
|
||||||
|
@ -37,15 +56,16 @@ export class Stream {
|
||||||
this.verticalResolution = verticalResolution || undefined
|
this.verticalResolution = verticalResolution || undefined
|
||||||
this.isInterlaced = isInterlaced || undefined
|
this.isInterlaced = isInterlaced || undefined
|
||||||
this.url = data.url
|
this.url = data.url
|
||||||
this.httpReferrer = data.http.referrer || undefined
|
this.referrer = data.http.referrer || undefined
|
||||||
this.httpUserAgent = data.http['user-agent'] || undefined
|
this.userAgent = data.http['user-agent'] || undefined
|
||||||
this.groupTitle = 'Undefined'
|
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
withChannel(channelsGroupedById: Dictionary): this {
|
withChannel(channelsKeyById: Dictionary): this {
|
||||||
if (!this.channelId) return this
|
if (!this.channelId) return this
|
||||||
|
|
||||||
this.channel = channelsGroupedById.get(this.channelId)
|
this.channel = channelsKeyById.get(this.channelId)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -93,18 +113,22 @@ export class Stream {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setHttpUserAgent(httpUserAgent: string): this {
|
setUserAgent(userAgent: string): this {
|
||||||
this.httpUserAgent = httpUserAgent
|
this.userAgent = userAgent
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setHttpReferrer(httpReferrer: string): this {
|
setReferrer(referrer: string): this {
|
||||||
this.httpReferrer = httpReferrer
|
this.referrer = referrer
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLine(): number {
|
||||||
|
return this.line || -1
|
||||||
|
}
|
||||||
|
|
||||||
setFilepath(filepath: string): this {
|
setFilepath(filepath: string): this {
|
||||||
this.filepath = filepath
|
this.filepath = filepath
|
||||||
|
|
||||||
|
@ -133,12 +157,12 @@ export class Stream {
|
||||||
return this.filepath || ''
|
return this.filepath || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getHttpReferrer(): string {
|
getReferrer(): string {
|
||||||
return this.httpReferrer || ''
|
return this.referrer || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getHttpUserAgent(): string {
|
getUserAgent(): string {
|
||||||
return this.httpUserAgent || ''
|
return this.userAgent || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuality(): string {
|
getQuality(): string {
|
||||||
|
@ -198,14 +222,6 @@ export class Stream {
|
||||||
return Object.assign(Object.create(Object.getPrototypeOf(this)), this)
|
return Object.assign(Object.create(Object.getPrototypeOf(this)), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasName(): boolean {
|
|
||||||
return !!this.name
|
|
||||||
}
|
|
||||||
|
|
||||||
noName(): boolean {
|
|
||||||
return !this.name
|
|
||||||
}
|
|
||||||
|
|
||||||
hasChannel() {
|
hasChannel() {
|
||||||
return !!this.channel
|
return !!this.channel
|
||||||
}
|
}
|
||||||
|
@ -281,8 +297,12 @@ export class Stream {
|
||||||
return this?.channel?.logo || ''
|
return this?.channel?.logo || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name || ''
|
||||||
|
}
|
||||||
|
|
||||||
getTitle(): string {
|
getTitle(): string {
|
||||||
let title = `${this.name}`
|
let title = `${this.getName()}`
|
||||||
|
|
||||||
if (this.getQuality()) {
|
if (this.getQuality()) {
|
||||||
title += ` (${this.getQuality()})`
|
title += ` (${this.getQuality()})`
|
||||||
|
@ -303,30 +323,13 @@ export class Stream {
|
||||||
return this.id || ''
|
return this.id || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
channel: this.channel,
|
|
||||||
feed: this.feed,
|
|
||||||
filepath: this.filepath,
|
|
||||||
label: this.label,
|
|
||||||
name: this.name,
|
|
||||||
verticalResolution: this.verticalResolution,
|
|
||||||
isInterlaced: this.isInterlaced,
|
|
||||||
url: this.url,
|
|
||||||
httpReferrer: this.httpReferrer,
|
|
||||||
httpUserAgent: this.httpUserAgent,
|
|
||||||
line: this.line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
channel: this.channelId || null,
|
channel: this.channelId || null,
|
||||||
feed: this.feedId || null,
|
feed: this.feedId || null,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
referrer: this.httpReferrer || null,
|
referrer: this.referrer || null,
|
||||||
user_agent: this.httpUserAgent || null,
|
user_agent: this.userAgent || null,
|
||||||
quality: this.getQuality() || null
|
quality: this.getQuality() || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,25 +341,25 @@ export class Stream {
|
||||||
output += ` tvg-logo="${this.getLogo()}" group-title="${this.groupTitle}"`
|
output += ` tvg-logo="${this.getLogo()}" group-title="${this.groupTitle}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.httpReferrer) {
|
if (this.referrer) {
|
||||||
output += ` http-referrer="${this.httpReferrer}"`
|
output += ` http-referrer="${this.referrer}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.httpUserAgent) {
|
if (this.userAgent) {
|
||||||
output += ` http-user-agent="${this.httpUserAgent}"`
|
output += ` http-user-agent="${this.userAgent}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
output += `,${this.getTitle()}`
|
output += `,${this.getTitle()}`
|
||||||
|
|
||||||
if (this.httpReferrer) {
|
if (this.referrer) {
|
||||||
output += `\n#EXTVLCOPT:http-referrer=${this.httpReferrer}`
|
output += `\r\n#EXTVLCOPT:http-referrer=${this.referrer}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.httpUserAgent) {
|
if (this.userAgent) {
|
||||||
output += `\n#EXTVLCOPT:http-user-agent=${this.httpUserAgent}`
|
output += `\r\n#EXTVLCOPT:http-user-agent=${this.userAgent}`
|
||||||
}
|
}
|
||||||
|
|
||||||
output += `\n${this.url}`
|
output += `\r\n${this.url}`
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
@ -379,7 +382,11 @@ function escapeRegExp(text) {
|
||||||
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseQuality(quality: string): { verticalResolution: number; isInterlaced: boolean } {
|
function parseQuality(quality: string | null): {
|
||||||
|
verticalResolution: number | null
|
||||||
|
isInterlaced: boolean | null
|
||||||
|
} {
|
||||||
|
if (!quality) return { verticalResolution: null, isInterlaced: null }
|
||||||
let [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
|
let [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined]
|
||||||
const isInterlaced = /i$/i.test(quality)
|
const isInterlaced = /i$/i.test(quality)
|
||||||
let verticalResolution = 0
|
let verticalResolution = 0
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
|
import { SubdivisionData, SubdivisionSerializedData } from '../types/subdivision'
|
||||||
import { Dictionary } from '@freearhey/core'
|
import { Dictionary } from '@freearhey/core'
|
||||||
import { Country } from '.'
|
import { Country } from '.'
|
||||||
|
|
||||||
type SubdivisionData = {
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
country: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Subdivision {
|
export class Subdivision {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
countryCode: string
|
countryCode: string
|
||||||
country?: Country
|
country?: Country
|
||||||
|
|
||||||
constructor(data: SubdivisionData) {
|
constructor(data?: SubdivisionData) {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
this.code = data.code
|
this.code = data.code
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.countryCode = data.country
|
this.countryCode = data.country
|
||||||
}
|
}
|
||||||
|
|
||||||
withCountry(countriesGroupedByCode: Dictionary): this {
|
withCountry(countriesKeyByCode: Dictionary): this {
|
||||||
this.country = countriesGroupedByCode.get(this.countryCode)
|
this.country = countriesKeyByCode.get(this.countryCode)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): SubdivisionSerializedData {
|
||||||
|
return {
|
||||||
|
code: this.code,
|
||||||
|
name: this.name,
|
||||||
|
countryCode: this.code,
|
||||||
|
country: this.country ? this.country.serialize() : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: SubdivisionSerializedData): this {
|
||||||
|
this.code = data.code
|
||||||
|
this.name = data.name
|
||||||
|
this.countryCode = data.countryCode
|
||||||
|
this.country = data.country ? new Country().deserialize(data.country) : undefined
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@ export class Timezone {
|
||||||
this.countryCodes = new Collection(data.countries)
|
this.countryCodes = new Collection(data.countries)
|
||||||
}
|
}
|
||||||
|
|
||||||
withCountries(countriesGroupedByCode: Dictionary): this {
|
withCountries(countriesKeyByCode: Dictionary): this {
|
||||||
this.countries = this.countryCodes.map((code: string) => countriesGroupedByCode.get(code))
|
this.countries = this.countryCodes.map((code: string) => countriesKeyByCode.get(code))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
5
scripts/types/blocklistRecord.d.ts
vendored
Normal file
5
scripts/types/blocklistRecord.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type BlocklistRecordData = {
|
||||||
|
channel: string
|
||||||
|
reason: string
|
||||||
|
ref: string
|
||||||
|
}
|
9
scripts/types/category.d.ts
vendored
Normal file
9
scripts/types/category.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export type CategorySerializedData = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryData = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
52
scripts/types/channel.d.ts
vendored
Normal file
52
scripts/types/channel.d.ts
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
import type { CountrySerializedData } from './country'
|
||||||
|
import type { SubdivisionSerializedData } from './subdivision'
|
||||||
|
import type { CategorySerializedData } from './category'
|
||||||
|
|
||||||
|
export type ChannelSerializedData = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
altNames: string[]
|
||||||
|
network?: string
|
||||||
|
owners: string[]
|
||||||
|
countryCode: string
|
||||||
|
country?: CountrySerializedData
|
||||||
|
subdivisionCode?: string
|
||||||
|
subdivision?: SubdivisionSerializedData
|
||||||
|
cityName?: string
|
||||||
|
categoryIds: string[]
|
||||||
|
categories?: CategorySerializedData[]
|
||||||
|
isNSFW: boolean
|
||||||
|
launched?: string
|
||||||
|
closed?: string
|
||||||
|
replacedBy?: string
|
||||||
|
website?: string
|
||||||
|
logo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type 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
|
||||||
|
logo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChannelSearchableData = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
altNames: string[]
|
||||||
|
guideNames: string[]
|
||||||
|
streamNames: string[]
|
||||||
|
feedFullNames: string[]
|
||||||
|
}
|
20
scripts/types/country.d.ts
vendored
Normal file
20
scripts/types/country.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import type { LanguageSerializedData } from './language'
|
||||||
|
import type { SubdivisionSerializedData } from './subdivision'
|
||||||
|
import type { RegionSerializedData } from './region'
|
||||||
|
|
||||||
|
export type CountrySerializedData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
flag: string
|
||||||
|
languageCode: string
|
||||||
|
language: LanguageSerializedData | null
|
||||||
|
subdivisions: SubdivisionSerializedData[]
|
||||||
|
regions: RegionSerializedData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CountryData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
lang: string
|
||||||
|
flag: string
|
||||||
|
}
|
19
scripts/types/dataLoader.d.ts
vendored
Normal file
19
scripts/types/dataLoader.d.ts
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Storage } from '@freearhey/core'
|
||||||
|
|
||||||
|
export type DataLoaderProps = {
|
||||||
|
storage: Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
export type 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[]
|
||||||
|
}
|
27
scripts/types/dataProcessor.d.ts
vendored
Normal file
27
scripts/types/dataProcessor.d.ts
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Collection, Dictionary } from '@freearhey/core'
|
||||||
|
|
||||||
|
export type DataProcessorData = {
|
||||||
|
blocklistRecordsGroupedByChannelId: Dictionary
|
||||||
|
subdivisionsGroupedByCountryCode: Dictionary
|
||||||
|
feedsGroupedByChannelId: Dictionary
|
||||||
|
guidesGroupedByStreamId: Dictionary
|
||||||
|
subdivisionsKeyByCode: Dictionary
|
||||||
|
countriesKeyByCode: Dictionary
|
||||||
|
languagesKeyByCode: Dictionary
|
||||||
|
streamsGroupedById: Dictionary
|
||||||
|
categoriesKeyById: Dictionary
|
||||||
|
timezonesKeyById: Dictionary
|
||||||
|
regionsKeyByCode: Dictionary
|
||||||
|
blocklistRecords: Collection
|
||||||
|
channelsKeyById: Dictionary
|
||||||
|
subdivisions: Collection
|
||||||
|
categories: Collection
|
||||||
|
countries: Collection
|
||||||
|
languages: Collection
|
||||||
|
timezones: Collection
|
||||||
|
channels: Collection
|
||||||
|
regions: Collection
|
||||||
|
streams: Collection
|
||||||
|
guides: Collection
|
||||||
|
feeds: Collection
|
||||||
|
}
|
12
scripts/types/feed.d.ts
vendored
Normal file
12
scripts/types/feed.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { Collection } from '@freearhey/core'
|
||||||
|
|
||||||
|
export type FeedData = {
|
||||||
|
channel: string
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
is_main: boolean
|
||||||
|
broadcast_area: Collection
|
||||||
|
languages: Collection
|
||||||
|
timezones: Collection
|
||||||
|
video_format: string
|
||||||
|
}
|
17
scripts/types/guide.d.ts
vendored
Normal file
17
scripts/types/guide.d.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export type GuideSerializedData = {
|
||||||
|
channelId?: string
|
||||||
|
feedId?: string
|
||||||
|
siteDomain: string
|
||||||
|
siteId: string
|
||||||
|
siteName: string
|
||||||
|
languageCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuideData = {
|
||||||
|
channel: string
|
||||||
|
feed: string
|
||||||
|
site: string
|
||||||
|
site_id: string
|
||||||
|
site_name: string
|
||||||
|
lang: string
|
||||||
|
}
|
9
scripts/types/language.d.ts
vendored
Normal file
9
scripts/types/language.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export type LanguageSerializedData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LanguageData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
}
|
13
scripts/types/region.d.ts
vendored
Normal file
13
scripts/types/region.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export type RegionSerializedData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
countryCodes: string[]
|
||||||
|
countries?: CountrySerializedData[]
|
||||||
|
subdivisions?: SubdivisionSerializedData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RegionData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
countries: string[]
|
||||||
|
}
|
10
scripts/types/stream.d.ts
vendored
Normal file
10
scripts/types/stream.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export type StreamData = {
|
||||||
|
channel: string | null
|
||||||
|
feed: string | null
|
||||||
|
name: string | null
|
||||||
|
url: string
|
||||||
|
referrer: string | null
|
||||||
|
user_agent: string | null
|
||||||
|
quality: string | null
|
||||||
|
label: string | null
|
||||||
|
}
|
12
scripts/types/subdivision.d.ts
vendored
Normal file
12
scripts/types/subdivision.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export type SubdivisionSerializedData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
countryCode: string
|
||||||
|
country?: CountrySerializedData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubdivisionData = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
country: string
|
||||||
|
}
|
57
tests/__data__/expected/api_generate/.api/streams.json
Normal file
57
tests/__data__/expected/api_generate/.api/streams.json
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"channel": null,
|
||||||
|
"feed": null,
|
||||||
|
"url": "http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8",
|
||||||
|
"referrer": null,
|
||||||
|
"user_agent": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel": null,
|
||||||
|
"feed": null,
|
||||||
|
"url": "http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8",
|
||||||
|
"referrer": "http://imn.iq",
|
||||||
|
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel": "AndorraTV.ad",
|
||||||
|
"feed": "SD",
|
||||||
|
"url": "https://iptv-all.lanesh4d0w.repl.co/andorra/atv",
|
||||||
|
"referrer": null,
|
||||||
|
"user_agent": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel": "BBCNews.uk",
|
||||||
|
"url": "http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8",
|
||||||
|
"referrer": null,
|
||||||
|
"user_agent": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel": "LDPRTV.ru",
|
||||||
|
"feed": null,
|
||||||
|
"url": "http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8",
|
||||||
|
"referrer": null,
|
||||||
|
"user_agent": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel": "MeteoMedia.ca",
|
||||||
|
"feed": null,
|
||||||
|
"url": "http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8",
|
||||||
|
"referrer": null,
|
||||||
|
"user_agent": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel": "VisitXTV.nl",
|
||||||
|
"feed": null,
|
||||||
|
"url": "https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8",
|
||||||
|
"referrer": null,
|
||||||
|
"user_agent": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel": "Zoo.ad",
|
||||||
|
"feed": null,
|
||||||
|
"url": "https://iptv-all.lanesh4d0w.repl.co/andorra/zoo",
|
||||||
|
"referrer": null,
|
||||||
|
"user_agent": null
|
||||||
|
}
|
||||||
|
]
|
|
@ -1,3 +1,3 @@
|
||||||
#EXTM3U
|
#EXTM3U
|
||||||
#EXTINF:-1 tvg-id="",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7]
|
#EXTINF:-1 tvg-id="",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7]
|
||||||
https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8
|
https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
#EXTM3U
|
#EXTM3U
|
||||||
#EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked]
|
#EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked]
|
||||||
http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8
|
http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8
|
||||||
#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (342p)
|
#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (342p)
|
||||||
http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8
|
http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8
|
||||||
#EXTINF:-1 tvg-id="NPO2.nl" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",NPO 2 (302p) [Geo-blocked]
|
#EXTINF:-1 tvg-id="NPO2.nl" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",NPO 2 (302p) [Geo-blocked]
|
||||||
#EXTVLCOPT:http-referrer=http://imn.iq
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8
|
http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8
|
||||||
#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 [Geo-blocked]
|
#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 [Geo-blocked]
|
||||||
http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8
|
http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
|
@ -0,0 +1,15 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="VisitXTV.nl" tvg-logo="https://i.imgur.com/RJ9wbNF.jpg" group-title="XXX",Visit-X TV
|
||||||
|
https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
|
@ -0,0 +1,11 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
|
@ -0,0 +1,25 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="News",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
|
@ -0,0 +1,31 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Andorra",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Canada",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kazakhstan",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kyrgyzstan",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russia",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Tajikistan",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Turkmenistan",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Uzbekistan",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="International",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="International",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
|
@ -0,0 +1,23 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Catalan",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="English",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russian",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Undefined",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Undefined",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1,23 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1,43 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Americas",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Asia",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Asia",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Central Asia",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Commonwealth of Independent States",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Commonwealth of Independent States",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Europe",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Europe, the Middle East and Africa",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe, the Middle East and Africa",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe, the Middle East and Africa",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="North America",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Northern America",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="International",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="International",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
|
@ -0,0 +1,17 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7]
|
||||||
|
#EXTVLCOPT:http-referrer=http://imn.iq
|
||||||
|
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd
|
||||||
|
#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV
|
||||||
|
http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p)
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/zoo
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1,5 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1,5 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1,7 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1,7 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV
|
||||||
|
https://iptv-all.lanesh4d0w.repl.co/andorra/atv
|
||||||
|
#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p)
|
||||||
|
http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7]
|
||||||
|
http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1,5 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD
|
||||||
|
http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8
|
||||||
|
#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i)
|
||||||
|
http://146.59.85.40:89/dunaworld/index.m3u8
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia
|
||||||
|
http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
|
@ -0,0 +1 @@
|
||||||
|
#EXTM3U
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue