-
-
-
- {#each guides as guide, index}
-
- {/each}
+
+
+ {#each feeds.all() as feed, index (feed.getUUID())}
+
+ {/each}
+
-
-
- {/if}
-
-
+
+ {/if}
+
+
+{/if}
diff --git a/src/store.js b/src/store.js
deleted file mode 100644
index 8e3d3ffdb..000000000
--- a/src/store.js
+++ /dev/null
@@ -1,276 +0,0 @@
-import { writable, get } from 'svelte/store'
-import { Playlist, Link } from 'iptv-playlist-generator'
-import sj from '@freearhey/search-js'
-import _ from 'lodash'
-import { browser } from '$app/environment'
-import { Channel } from './models'
-import { pushState } from '$app/navigation'
-
-export const query = writable('')
-export const hasQuery = writable(false)
-export const channels = writable([])
-export const countries = writable({})
-export const filteredChannels = writable([])
-export const selected = writable([])
-export const downloadMode = writable(false)
-
-let searchIndex = {}
-export function search(q) {
- if (!q) {
- filteredChannels.set(get(channels))
- hasQuery.set(false)
- return
- }
-
- if (searchIndex.search) {
- let results = searchIndex.search(q)
- filteredChannels.set(results)
- hasQuery.set(true)
- }
-}
-
-export async function fetchChannels() {
- const api = await loadAPI()
-
- countries.set(api.countries)
-
- let _channels = api.channels.map(c => createChannel(c, api))
-
- channels.set(_channels)
- filteredChannels.set(_channels)
- searchIndex = sj.createIndex(_channels, {
- searchable: [
- 'id',
- 'name',
- 'alt_names',
- 'alt_name',
- 'network',
- 'owner',
- 'owners',
- 'country',
- 'subdivision',
- 'city',
- 'broadcast_area',
- 'language',
- 'languages',
- 'category',
- 'categories',
- 'launched',
- 'closed',
- 'replaced_by',
- 'website',
- 'streams',
- 'guides',
- 'is_nsfw',
- 'is_closed',
- 'is_blocked',
- '_guideNames',
- '_streamUrls'
- ]
- })
-}
-
-export function setSearchParam(key, value) {
- let query = key && value ? `?${key}=${value}` : ''
- query = query.replace(/\+/g, '%2B')
- const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}${query}`
- const state = {}
- state[key] = value
- pushState(url, state)
- setPageTitle(value)
-}
-
-export function setPageTitle(value) {
- if (browser) {
- const title = value ? `${value} · iptv-org` : 'iptv-org'
- window.document.title = title
- }
-}
-
-export function createChannel(data, api) {
- let broadcastArea = []
- let regionCountries = []
-
- data.broadcast_area.forEach(areaCode => {
- const [type, code] = areaCode.split('/')
- switch (type) {
- case 'c':
- const country = api.countries[code]
- if (country) broadcastArea.push({ type, code: country.code, name: country.name })
- break
- case 'r':
- const region = api.regions[code]
- if (region) {
- broadcastArea.push({ type, code: region.code, name: region.name })
- regionCountries = [
- ...regionCountries,
- ...region.countries.map(code => api.countries[code]).filter(Boolean)
- ]
- }
- break
- case 's':
- const subdivision = api.subdivisions[code]
- if (subdivision)
- broadcastArea.push({ type, code: subdivision.code, name: subdivision.name })
- break
- }
- })
-
- return new Channel({
- id: data.id,
- name: data.name,
- altNames: data.alt_names,
- network: data.network,
- owners: data.owners,
- city: data.city,
- country: api.countries[data.country],
- subdivision: api.subdivisions[data.subdivision],
- languages: data.languages.map(code => api.languages[code]).filter(Boolean),
- categories: data.categories.map(id => api.categories[id]).filter(Boolean),
- isNSFW: data.is_nsfw,
- launched: data.launched,
- closed: data.closed,
- replacedBy: data.replaced_by,
- website: data.website,
- logo: data.logo,
- streams: api.streams[data.id],
- guides: api.guides[data.id],
- blocklistRecords: api.blocklist[data.id],
- hasUniqueName: api.nameIndex[data.name.toLowerCase()].length === 1,
- broadcastArea,
- regionCountries
- })
-}
-
-async function loadAPI() {
- const api = {}
-
- const [
- countries,
- regions,
- subdivisions,
- languages,
- categories,
- streams,
- blocklist,
- channels,
- guides
- ] = await Promise.all([
- fetch('https://iptv-org.github.io/api/countries.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data =>
- data.map(i => {
- i.expanded = false
- return i
- })
- )
- .then(data => _.keyBy(data, 'code')),
- fetch('https://iptv-org.github.io/api/regions.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data => _.keyBy(data, 'code')),
- fetch('https://iptv-org.github.io/api/subdivisions.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data => _.keyBy(data, 'code')),
- fetch('https://iptv-org.github.io/api/languages.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data => _.keyBy(data, 'code')),
- fetch('https://iptv-org.github.io/api/categories.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data => _.keyBy(data, 'id')),
- fetch('https://iptv-org.github.io/api/streams.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data => _.groupBy(data, 'channel')),
- fetch('https://iptv-org.github.io/api/blocklist.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data => _.groupBy(data, 'channel')),
- fetch('https://iptv-org.github.io/api/channels.json')
- .then(r => r.json())
- .then(data => (data.length ? data : [])),
- fetch('https://iptv-org.github.io/api/guides.json')
- .then(r => r.json())
- .then(data => (data.length ? data : []))
- .then(data => data.filter(guide => guide.channel))
- .then(data => _.sortBy(data, 'lang'))
- .then(data => _.groupBy(data, 'channel'))
- ])
-
- api.countries = countries
- api.regions = regions
- api.subdivisions = subdivisions
- api.languages = languages
- api.categories = categories
- api.streams = streams
- api.blocklist = blocklist
- api.channels = channels
- api.guides = guides
-
- api.nameIndex = _.groupBy(api.channels, channel => channel.name.toLowerCase())
-
- return api
-}
-
-function getStreams() {
- let streams = []
- get(selected).forEach(channel => {
- channel._streams.forEach(stream => {
- if (stream.status === 'error') return
-
- stream.channel = channel
- streams.push(stream)
- })
- })
-
- const levels = { online: 1, blocked: 2, timeout: 3, error: 4, default: 5 }
- streams = _.orderBy(
- streams,
- [
- s => s.channel.id.toLowerCase(),
- s => levels[s.status] || levels['default'],
- 'height',
- 'frame_rate',
- 'url'
- ],
- ['asc', 'asc', 'desc', 'desc', 'asc']
- )
- streams = _.uniqBy(streams, stream => stream.channel.id || _.uniqueId())
-
- return streams
-}
-
-export function createPlaylist() {
- const playlist = new Playlist()
-
- let streams = getStreams()
- streams.forEach(stream => {
- const link = new Link(stream.url)
- link.title = stream.channel.name
- link.attrs = {
- 'tvg-id': stream.channel.id,
- 'tvg-logo': stream.channel.logo,
- 'group-title': stream.channel._categories
- .map(channel => channel.name)
- .sort()
- .join(';')
- }
-
- if (stream.user_agent) {
- link.attrs['user-agent'] = stream.user_agent
- link.vlcOpts['http-user-agent'] = stream.user_agent
- }
-
- if (stream.http_referrer) {
- link.vlcOpts['http-referrer'] = stream.http_referrer
- }
-
- playlist.links.push(link)
- })
-
- return playlist
-}
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 000000000..cf8214e8e
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,43 @@
+import { writable, get, type Writable } from 'svelte/store'
+import { Collection } from '@freearhey/core/browser'
+import { DataLoader, SearchEngine } from './core'
+import { Channel } from '~/models'
+
+export const query = writable('')
+export const hasQuery = writable(false)
+export const countries: Writable
= writable(new Collection())
+export const channels: Writable = writable(new Collection())
+export const searchResults: Writable = writable(new Collection())
+export const selected: Writable = writable(new Collection())
+export const downloadMode = writable(false)
+export const isSearching = writable(false)
+export const isLoading = writable(true)
+export const isReady = writable(false)
+
+const searchEngine = new SearchEngine()
+
+export function search(query: string) {
+ isSearching.set(true)
+ hasQuery.set(!!query)
+ if (!query) {
+ searchResults.set(get(channels))
+ isSearching.set(false)
+ return
+ }
+
+ let results = searchEngine.search(query)
+ searchResults.set(results)
+ isSearching.set(false)
+}
+
+export async function loadData({ client, processor }) {
+ const dataLoader = new DataLoader({ client, processor })
+ const data = await dataLoader.load()
+
+ countries.set(data.countries)
+ channels.set(data.channels)
+ searchResults.set(data.channels)
+
+ const searchableData = data.channels.map((channel: Channel) => channel.getSearchable())
+ searchEngine.createIndex(searchableData)
+}
diff --git a/src/types/blocklistRecord.d.ts b/src/types/blocklistRecord.d.ts
new file mode 100644
index 000000000..be5a24b97
--- /dev/null
+++ b/src/types/blocklistRecord.d.ts
@@ -0,0 +1,11 @@
+export type BlocklistRecordSerializedData = {
+ channelId: string
+ reason: string
+ refUrl: string
+}
+
+export type BlocklistRecordData = {
+ channel: string
+ reason: string
+ ref: string
+}
diff --git a/src/types/broadcastArea.d.ts b/src/types/broadcastArea.d.ts
new file mode 100644
index 000000000..13b0c4450
--- /dev/null
+++ b/src/types/broadcastArea.d.ts
@@ -0,0 +1,15 @@
+import type { SubdivisionSerializedData } from './subdivision'
+import type { CountrySerializedData } from './country'
+import type { RegionSerializedData } from './region'
+
+export type BroadcastAreaSerializedData = {
+ code: string
+ name: string
+ countries: CountrySerializedData[]
+ subdivisions: SubdivisionSerializedData[]
+ regions: RegionSerializedData[]
+}
+
+export type BroadcastAreaData = {
+ code: string
+}
diff --git a/src/types/category.d.ts b/src/types/category.d.ts
new file mode 100644
index 000000000..e78d6c62e
--- /dev/null
+++ b/src/types/category.d.ts
@@ -0,0 +1,9 @@
+export type CategorySerializedData = {
+ id: string
+ name: string
+}
+
+export type CategoryData = {
+ id: string
+ name: string
+}
diff --git a/src/types/channel.d.ts b/src/types/channel.d.ts
new file mode 100644
index 000000000..9879be9ad
--- /dev/null
+++ b/src/types/channel.d.ts
@@ -0,0 +1,87 @@
+import type { BlocklistRecordSerializedData } from './blocklistRecord'
+import type { CategorySerializedData } from './category'
+import type { CountrySerializedData } from './country'
+import type { FeedSerializedData } from './feed'
+import type { SubdivisionSerializedData } from './subdivision'
+
+export type ChannelSearchable = {
+ id: string
+ name: string
+ alt_names: string[]
+ alt_name: string[]
+ network: string
+ owner: string[]
+ owners: string[]
+ country: string
+ subdivision: string
+ city: string
+ category: string[]
+ categories: string[]
+ launched: string
+ closed: string
+ replaced_by: string
+ website: string
+ is_nsfw: boolean
+ is_closed: boolean
+ is_blocked: boolean
+ languages: string[]
+ language: string[]
+ broadcast_area: string[]
+ streams: number
+ guides: number
+ feeds: number
+ video_format: string[]
+ video_formats: string[]
+ timezone: string[]
+ timezones: string[]
+ _languageNames: string[]
+ _broadcastLocationCodes: string[]
+ _broadcastLocationNames: string[]
+ _countryName: string
+ _guideSiteNames: string[]
+ _streamUrls: string[]
+}
+
+export type ChannelSerializedData = {
+ id: string
+ name: string
+ altNames: string[]
+ networkName?: string
+ ownerNames: string[]
+ countryCode: string
+ country?: CountrySerializedData
+ subdivisionCode?: string
+ subdivision?: SubdivisionSerializedData
+ cityName?: string
+ categoryIds: string[]
+ categories: CategorySerializedData[]
+ isNSFW: boolean
+ launchedDateString?: string
+ launchedDate?: string
+ closedDateString?: string
+ closedDate?: string
+ replacedByChannelId?: string
+ websiteUrl?: string
+ logoUrl: string
+ blocklistRecords: BlocklistRecordSerializedData[]
+ feeds: FeedSerializedData[]
+ hasUniqueName: boolean
+}
+
+export type ChannelData = {
+ id: string
+ name: string
+ alt_names: string[]
+ network: string
+ owners: string[]
+ country: string
+ subdivision: string
+ city: string
+ categories: string[]
+ is_nsfw: boolean
+ launched: string
+ closed: string
+ replaced_by: string
+ website: string
+ logo: string
+}
diff --git a/src/types/country.d.ts b/src/types/country.d.ts
new file mode 100644
index 000000000..61cbece44
--- /dev/null
+++ b/src/types/country.d.ts
@@ -0,0 +1,13 @@
+export type CountrySerializedData = {
+ code: string
+ name: string
+ flagEmoji: string
+ languageCode: string
+}
+
+export type CountryData = {
+ code: string
+ name: string
+ flag: string
+ lang: string
+}
diff --git a/src/types/feed.d.ts b/src/types/feed.d.ts
new file mode 100644
index 000000000..6cb091d69
--- /dev/null
+++ b/src/types/feed.d.ts
@@ -0,0 +1,32 @@
+import type { BroadcastAreaSerializedData } from './broadcastArea'
+import type { LanguageSerializedData } from './language'
+import type { StreamSerializedData } from './stream'
+import type { GuideSerializedData } from './guide'
+import type { ChannelSerializedData } from './channel'
+
+export type FeedSerializedData = {
+ channelId: string
+ channel: ChannelSerializedData
+ id: string
+ name: string
+ isMain: boolean
+ broadcastAreaCodes: string[]
+ broadcastArea: BroadcastAreaSerializedData[]
+ timezoneIds: string[]
+ languageCodes: string[]
+ languages: LanguageSerializedData[]
+ videoFormat: string
+ streams: StreamSerializedData[]
+ guides: GuideSerializedData[]
+}
+
+export type FeedData = {
+ channel: string
+ id: string
+ name: string
+ is_main: boolean
+ broadcast_area: string[]
+ timezones: string[]
+ languages: string[]
+ video_format?: string
+}
diff --git a/src/types/guide.d.ts b/src/types/guide.d.ts
new file mode 100644
index 000000000..63a6ecdb1
--- /dev/null
+++ b/src/types/guide.d.ts
@@ -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
+}
diff --git a/src/types/htmlPreviewField.ts b/src/types/htmlPreviewField.ts
new file mode 100644
index 000000000..a3c07bf4d
--- /dev/null
+++ b/src/types/htmlPreviewField.ts
@@ -0,0 +1,8 @@
+export type HTMLPreviewImage = { src: string; alt: string; title: string }
+export type HTMLPreviewLink = { label: string; query: string }
+export type HTMLPreviewExternalLink = { label: string; title: string; href: string }
+export type HTMLPreviewField = {
+ name: string
+ type: string
+ value: HTMLPreviewImage | HTMLPreviewLink | HTMLPreviewLink[] | HTMLPreviewExternalLink | string
+}
diff --git a/src/types/jsonDataViewerField.ts b/src/types/jsonDataViewerField.ts
new file mode 100644
index 000000000..97c96f57d
--- /dev/null
+++ b/src/types/jsonDataViewerField.ts
@@ -0,0 +1,4 @@
+export type JsonDataViewerField = {
+ name: string
+ value: any
+}
diff --git a/src/types/language.d.ts b/src/types/language.d.ts
new file mode 100644
index 000000000..2b9d4525c
--- /dev/null
+++ b/src/types/language.d.ts
@@ -0,0 +1,9 @@
+export type LanguageSerializedData = {
+ code: string
+ name: string
+}
+
+export type LanguageData = {
+ code: string
+ name: string
+}
diff --git a/src/types/region.d.ts b/src/types/region.d.ts
new file mode 100644
index 000000000..9f99f73dc
--- /dev/null
+++ b/src/types/region.d.ts
@@ -0,0 +1,11 @@
+export type RegionSerializedData = {
+ code: string
+ name: string
+ countryCodes: string[]
+}
+
+export type RegionData = {
+ code: string
+ name: string
+ countries: string[]
+}
diff --git a/src/types/stream.d.ts b/src/types/stream.d.ts
new file mode 100644
index 000000000..05d3dfed1
--- /dev/null
+++ b/src/types/stream.d.ts
@@ -0,0 +1,17 @@
+export type StreamSerializedData = {
+ channelId?: string
+ feedId?: string
+ url: string
+ referrer?: string
+ userAgent?: string
+ quality?: string
+}
+
+export type StreamData = {
+ channel: string
+ feed: string
+ url: string
+ referrer: string
+ user_agent: string
+ quality: string
+}
diff --git a/src/types/subdivision.d.ts b/src/types/subdivision.d.ts
new file mode 100644
index 000000000..ee5251801
--- /dev/null
+++ b/src/types/subdivision.d.ts
@@ -0,0 +1,11 @@
+export type SubdivisionSerializedData = {
+ code: string
+ name: string
+ countryCode: string
+}
+
+export type SubdivisionData = {
+ code: string
+ name: string
+ country: string
+}
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 000000000..531543c2c
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,23 @@
+import { browser } from '$app/environment'
+import { pushState } from '$app/navigation'
+
+export function setSearchParam(key?: string, value?: string) {
+ let query = key && value ? `?${key}=${value}` : ''
+ query = query.replace(/\+/g, '%2B')
+ const url = `${window.location.protocol}//${window.location.host}/${query}`
+ const state: { [key: string]: string } = {}
+ state[key] = value
+ pushState(url, state)
+ setPageTitle(value)
+}
+
+export function setPageTitle(value?: string) {
+ if (browser) {
+ const title = value ? `${value} · iptv-org` : 'iptv-org'
+ window.document.title = title
+ }
+}
+
+export function pluralize(number: number, word: string) {
+ return number > 1 ? word + 's' : word
+}
diff --git a/src/utils/apiClient.js b/src/utils/apiClient.js
deleted file mode 100644
index c83b08909..000000000
--- a/src/utils/apiClient.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import axios from 'axios'
-import cliProgress from 'cli-progress'
-import numeral from 'numeral'
-
-export class ApiClient {
- constructor({ storage }) {
- this.storage = storage
- this.client = axios.create({
- responseType: 'stream'
- })
- 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) {
- const stream = await this.storage.createStream(filename)
-
- const bar = this.progressBar.create(0, 0, { filename })
-
- this.client
- .get(`https://iptv-org.github.io/api/${filename}`, {
- onDownloadProgress({ total, loaded, rate }) {
- if (total) bar.setTotal(total)
- bar.update(loaded, { speed: rate })
- }
- })
- .then(response => {
- response.data.pipe(stream)
- })
- }
-}