mirror of
https://github.com/iptv-org/epg
synced 2026-09-12 14:56:51 -04:00
continue cleaning + simplify mtel.ba (right now broken EPG on backend)
This commit is contained in:
Generated
+19
@@ -81,6 +81,7 @@
|
|||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"unzipit": "^1.4.3",
|
"unzipit": "^1.4.3",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"wildcard-match": "^5.1.4"
|
"wildcard-match": "^5.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -11418,6 +11419,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uzip-module": {
|
"node_modules/uzip-module": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
|
||||||
@@ -19318,6 +19332,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
},
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="
|
||||||
|
},
|
||||||
"uzip-module": {
|
"uzip-module": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"unzipit": "^1.4.3",
|
"unzipit": "^1.4.3",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"wildcard-match": "^5.1.4"
|
"wildcard-match": "^5.1.4"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.2"
|
"packageManager": "yarn@4.9.2"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { SiteConfig } from 'epg-grabber'
|
import { SiteConfig } from 'epg-grabber'
|
||||||
import { deepMerge } from '../functions'
|
|
||||||
import { pathToFileURL } from 'url'
|
import { pathToFileURL } from 'url'
|
||||||
|
|
||||||
export class ConfigLoader {
|
export class ConfigLoader {
|
||||||
@@ -28,6 +27,6 @@ export class ConfigLoader {
|
|||||||
channels: undefined
|
channels: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return deepMerge(defaultConfig, config) as SiteConfig
|
return { ...defaultConfig, ...config } as SiteConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,65 @@
|
|||||||
// Made to replace lodash functions with their native alternatives. Typed for better TypeScript support.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new array of unique items based on an specific identifier.
|
* Sorts an array by the result of running each element through an iteratee function.
|
||||||
* This function uses a Map to ensure that each item is unique based on the result of the provided function.
|
* Creates a shallow copy of the array before sorting to avoid mutating the original.
|
||||||
* @param {Array} arr - The array to filter for unique items
|
|
||||||
* @param {Function} fn - A function that takes an item and returns a unique identifier
|
|
||||||
* @returns {Array} A new array containing only unique items based on the identifier
|
|
||||||
* @example
|
|
||||||
* const items = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 1, name: 'C' }];
|
|
||||||
* const uniqueItems = uniqBy(items, item => item.id);
|
|
||||||
* // uniqueItems will be [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
|
|
||||||
*/
|
|
||||||
export const uniqBy = <T, K>(arr: T[], fn: (item: T) => K): T[] => [...new Map(arr.map(x => [fn(x), x])).values()]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively merges multiple objects into a single object.
|
|
||||||
* If the same key exists in multiple objects and the values are both objects,
|
|
||||||
* they will be deep merged. Otherwise, the latter value will override the former.
|
|
||||||
*
|
*
|
||||||
* @param {...object[]} a - An array of objects to be merged
|
* @param {Array} arr - The array to sort
|
||||||
* @returns {Record<string, unknown>} A new object containing all merged properties
|
* @param {Function} fn - The iteratee function to compute sort values
|
||||||
|
* @returns {Array} A new sorted array
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const obj1 = { a: { b: 2 }, c: 3 };
|
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}];
|
||||||
* const obj2 = { a: { d: 4 }, e: 5 };
|
* sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}]
|
||||||
* deepMerge(obj1, obj2); // { a: { b: 2, d: 4 }, c: 3, e: 5 }
|
|
||||||
*/
|
*/
|
||||||
export const deepMerge = (...a: (object)[]): Record<string, unknown> =>
|
export const sortBy = <T>(arr: T[], fn: (item: T) => number | string): T[] => [...arr].sort((a, b) => fn(a) > fn(b) ? 1 : -1)
|
||||||
a.reduce((r: { [key: string]: unknown }, o) =>
|
|
||||||
(Object.entries(o).forEach(([k, v]) => { r[k] = r[k] && typeof r[k] === 'object' && typeof v === 'object' ?
|
|
||||||
deepMerge(r[k], v) : v }), r), {} as Record<string, unknown>)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort an array of objects by a specific key.
|
* Sorts an array by multiple criteria with customizable sort orders.
|
||||||
|
* Supports ascending (default) and descending order for each criterion.
|
||||||
*
|
*
|
||||||
* @param {string} key - The key to sort by
|
* @param {Array} arr - The array to sort
|
||||||
* @returns {function} A comparison function for sorting
|
* @param {Array<Function>} fns - Array of iteratee functions to compute sort values
|
||||||
|
* @param {Array<string>} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc'
|
||||||
|
* @returns {Array} A new sorted array
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}];
|
||||||
|
* orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']);
|
||||||
|
* // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}]
|
||||||
*/
|
*/
|
||||||
export const sortBy = <T>(key: keyof T): ((a: T, b: T) => number) => {
|
export const orderBy = (arr: Array<unknown>, fns: Array<(item: unknown) => string | number>, orders: Array<string> = []): Array<unknown> => [...arr].sort((a, b) =>
|
||||||
return (a: T, b: T) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0)
|
fns.reduce((acc, fn, i) =>
|
||||||
}
|
acc || ((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a duplicate-free version of an array using an iteratee function to generate
|
||||||
|
* the criterion by which uniqueness is computed. Only the first occurrence of each
|
||||||
|
* element is kept.
|
||||||
|
*
|
||||||
|
* @param {Array} arr - The array to inspect
|
||||||
|
* @param {Function} fn - The iteratee function to compute uniqueness criterion
|
||||||
|
* @returns {Array} A new duplicate-free array
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const users = [{id: 1, name: 'john'}, {id: 2, name: 'jane'}, {id: 1, name: 'john'}];
|
||||||
|
* uniqBy(users, x => x.id); // [{id: 1, name: 'john'}, {id: 2, name: 'jane'}]
|
||||||
|
*/
|
||||||
|
export const uniqBy = <T>(arr: T[], fn: (item: T) => unknown): T[] => arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to start case (capitalizes the first letter of each word).
|
||||||
|
* Handles camelCase, snake_case, kebab-case, and regular spaces.
|
||||||
|
*
|
||||||
|
* @param {string} str - The string to convert
|
||||||
|
* @returns {string} The start case string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* startCase('hello_world'); // "Hello World"
|
||||||
|
* startCase('helloWorld'); // "Hello World"
|
||||||
|
* startCase('hello-world'); // "Hello World"
|
||||||
|
* startCase('hello world'); // "Hello World"
|
||||||
|
*/
|
||||||
|
export const startCase = (str: string): string => str
|
||||||
|
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase
|
||||||
|
.replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces
|
||||||
|
.replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { GuideData } from '../types/guide'
|
import type { GuideData } from '../types/guide'
|
||||||
import { uniqueId } from 'lodash'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
export class Guide {
|
export class Guide {
|
||||||
channelId?: string
|
channelId?: string
|
||||||
@@ -21,7 +21,7 @@ export class Guide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUUID(): string {
|
getUUID(): string {
|
||||||
if (!this.getStreamId() || !this.siteId) return uniqueId()
|
if (!this.getStreamId() || !this.siteId) return uuidv4()
|
||||||
|
|
||||||
return this.getStreamId() + this.siteId
|
return this.getStreamId() + this.siteId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return programs.concat().sort(sortBy('start'))
|
return sortBy(programs, p => p.start.valueOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const doFetch = require('@ntlab/sfetch')
|
const doFetch = require('@ntlab/sfetch')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const _ = require('lodash')
|
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
const { sortBy } = require('../../scripts/functions')
|
||||||
|
|
||||||
// API Configuration Constants
|
// API Configuration Constants
|
||||||
const NATCO_CODE = 'hr'
|
const NATCO_CODE = 'hr'
|
||||||
@@ -86,7 +86,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
items = _.sortBy(items, i => dayjs(i.start_time).valueOf())
|
items = sortBy(items, i => dayjs(i.start_time).valueOf())
|
||||||
|
|
||||||
// Fetch program details for each item
|
// Fetch program details for each item
|
||||||
const programs = []
|
const programs = []
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const _ = require('lodash')
|
|
||||||
const doFetch = require('@ntlab/sfetch')
|
const doFetch = require('@ntlab/sfetch')
|
||||||
const dayjs = require('dayjs')
|
const dayjs = require('dayjs')
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
|
const { sortBy } = require('../../scripts/functions')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
@@ -15,7 +15,7 @@ module.exports = {
|
|||||||
url({ channel, date }) {
|
url({ channel, date }) {
|
||||||
const [platform] = channel.site_id.split('#')
|
const [platform] = channel.site_id.split('#')
|
||||||
|
|
||||||
return `https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-${platform}¤tPage=0&pageSize=1000&date=${date.format(
|
return `https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-${platform}&pageSize=999&date=${date.format(
|
||||||
'YYYY-MM-DD'
|
'YYYY-MM-DD'
|
||||||
)}`
|
)}`
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,6 @@ module.exports = {
|
|||||||
let programs = []
|
let programs = []
|
||||||
const items = parseItems(content, channel)
|
const items = parseItems(content, channel)
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (item.title === 'Nema informacija o programu') return
|
|
||||||
programs.push({
|
programs.push({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
@@ -46,14 +45,14 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
async channels({ platform = 'msat' }) {
|
async channels({ platform = 'msat' }) {
|
||||||
const platforms = {
|
const platforms = {
|
||||||
msat: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=100¤tPage=<CURRENT_PAGE>&query=:relevantno:tv-kategorija:tv-msat',
|
msat: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-msat',
|
||||||
iptv: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=100¤tPage=<CURRENT_PAGE>&query=:relevantno:tv-kategorija:tv-iptv:tv-iptv-paket:Svi+kanali'
|
iptv: 'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/search?pageSize=999&query=:relevantno:tv-kategorija:tv-iptv'
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = [
|
const queue = [
|
||||||
{
|
{
|
||||||
platform,
|
platform,
|
||||||
url: platforms[platform].replace('<CURRENT_PAGE>', 0)
|
url: platforms[platform]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ module.exports = {
|
|||||||
if (data && data.pagination.currentPage < data.pagination.totalPages) {
|
if (data && data.pagination.currentPage < data.pagination.totalPages) {
|
||||||
queue.push({
|
queue.push({
|
||||||
platform: req.platform,
|
platform: req.platform,
|
||||||
url: platforms[req.platform].replace('<CURRENT_PAGE>', ++data.pagination.currentPage)
|
url: platforms[req.platform]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +101,9 @@ function parseItems(content, channel) {
|
|||||||
const [, channelId] = channel.site_id.split('#')
|
const [, channelId] = channel.site_id.split('#')
|
||||||
const channelData = data.products.find(channel => channel.code === channelId)
|
const channelData = data.products.find(channel => channel.code === channelId)
|
||||||
if (!channelData || !Array.isArray(channelData.programs)) return []
|
if (!channelData || !Array.isArray(channelData.programs)) return []
|
||||||
|
// filter out programs that have the sentence "no program information available"
|
||||||
return _.sortBy(channelData.programs, p => parseStart(p).valueOf())
|
channelData.programs = channelData.programs.filter(p => !p.title.includes('Nema informacija o programu'))
|
||||||
|
return sortBy(channelData.programs, p => parseStart(p).valueOf())
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const channel = { site_id: 'msat#ch-11-rtrs' }
|
|||||||
|
|
||||||
it('can generate valid url', () => {
|
it('can generate valid url', () => {
|
||||||
expect(url({ date, channel })).toBe(
|
expect(url({ date, channel })).toBe(
|
||||||
'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat¤tPage=0&pageSize=1000&date=2025-02-04'
|
'https://mtel.ba/hybris/ecommerce/b2c/v1/products/channels/epg?platform=tv-msat&pageSize=999&date=2025-02-04'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const cheerio = require('cheerio')
|
|||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require('dayjs/plugin/utc')
|
||||||
const timezone = require('dayjs/plugin/timezone')
|
const timezone = require('dayjs/plugin/timezone')
|
||||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||||
const _ = require('lodash')
|
const { startCase } = require('../../scripts/functions')
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
@@ -164,7 +164,7 @@ function parseDuration($item) {
|
|||||||
function parseItems(content, date) {
|
function parseItems(content, date) {
|
||||||
if (!content) return []
|
if (!content) return []
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
const d = _.startCase(date.locale('es').format('DD MMMM YYYY'))
|
const d = startCase(date.locale('es').format('DD MMMM YYYY'))
|
||||||
|
|
||||||
return $(`.trProg[title*="${d}"]`).toArray()
|
return $(`.trProg[title*="${d}"]`).toArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
programs = sortBy(uniqBy(programs, p => p.start), 'start')
|
programs = sortBy(uniqBy(programs, p => p.start), p => p.start.valueOf())
|
||||||
|
|
||||||
return programs
|
return programs
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user