Fix linter issues in sites/

This commit is contained in:
freearhey 2025-01-01 12:27:22 +03:00
parent d6d20b6413
commit 5df982bb7c
129 changed files with 3316 additions and 3226 deletions

View file

@ -92,7 +92,7 @@ function parseItems(content, channel) {
const [, channelId] = channel.site_id.split('#') const [, channelId] = channel.site_id.split('#')
const channelData = data.schedule.find(i => i.channel == channelId) const channelData = data.schedule.find(i => i.channel == channelId)
return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : [] return channelData.listing && Array.isArray(channelData.listing) ? channelData.listing : []
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -1,4 +1,4 @@
const { parser, url, request } = require('./awilime.com.config.js') const { parser, url } = require('./awilime.com.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')

View file

@ -1,5 +1,4 @@
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
@ -62,7 +61,7 @@ function parseItems(content) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch {
return [] return []
} }

View file

@ -1,6 +1,4 @@
const { parser, url } = require('./beinsports.com.config.js') const { parser, url } = require('./beinsports.com.config.js')
const fs = require('fs')
const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')

View file

@ -1,13 +1,12 @@
const axios = require('axios'); const cheerio = require('cheerio')
const cheerio = require('cheerio'); const dayjs = require('dayjs')
const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc')
const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone')
const timezone = require('dayjs/plugin/timezone'); const customParseFormat = require('dayjs/plugin/customParseFormat')
const customParseFormat = require('dayjs/plugin/customParseFormat');
dayjs.extend(utc); dayjs.extend(utc)
dayjs.extend(timezone); dayjs.extend(timezone)
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'chada.ma', site: 'chada.ma',
@ -19,36 +18,38 @@ module.exports = {
} }
}, },
url() { url() {
return 'https://chada.ma/fr/chada-tv/grille-tv/'; return 'https://chada.ma/fr/chada-tv/grille-tv/'
}, },
parser: function ({ content }) { parser: function ({ content }) {
const $ = cheerio.load(content); const $ = cheerio.load(content)
const programs = []; const programs = []
$('#stopfix .posts-area h2').each((i, element) => { $('#stopfix .posts-area h2').each((i, element) => {
const timeRange = $(element).text().trim(); const timeRange = $(element).text().trim()
const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim())); const [start, stop] = timeRange.split(' - ').map(t => parseProgramTime(t.trim()))
const titleElement = $(element).next('div').next('h3'); const titleElement = $(element).next('div').next('h3')
const title = titleElement.text().trim(); const title = titleElement.text().trim()
const description = titleElement.next('div').text().trim() || 'No description available'; const description = titleElement.next('div').text().trim() || 'No description available'
programs.push({ programs.push({
title, title,
description, description,
start, start,
stop stop
}); })
}); })
return programs; return programs
} }
}; }
function parseProgramTime(timeStr) { function parseProgramTime(timeStr) {
const timeZone = 'Africa/Casablanca'; const timeZone = 'Africa/Casablanca'
const currentDate = dayjs().format('YYYY-MM-DD'); const currentDate = dayjs().format('YYYY-MM-DD')
return dayjs.tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone).format('YYYY-MM-DDTHH:mm:ssZ'); return dayjs
.tz(`${currentDate} ${timeStr}`, 'YYYY-MM-DD HH:mm', timeZone)
.format('YYYY-MM-DDTHH:mm:ssZ')
} }

View file

@ -1,7 +1,5 @@
const { parser, url } = require('./chada.ma.config.js') const { parser, url } = require('./chada.ma.config.js')
const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
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')
@ -27,11 +25,11 @@ const mockHtmlContent = `
<div class="ssprogramme row"></div> <div class="ssprogramme row"></div>
</div> </div>
</div> </div>
`; `
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/') expect(url()).toBe('https://chada.ma/fr/chada-tv/grille-tv/')
}); })
it('can parse response', () => { it('can parse response', () => {
const content = mockHtmlContent const content = mockHtmlContent
@ -44,8 +42,8 @@ it('can parse response', () => {
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: "Bloc Prime + Clips", title: 'Bloc Prime + Clips',
description: "No description available", description: 'No description available',
start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'), start: dayjs.tz('00:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ'),
stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ') stop: dayjs.tz('09:00', 'HH:mm', 'Africa/Casablanca').format('YYYY-MM-DDTHH:mm:ssZ')
} }

View file

@ -29,7 +29,7 @@ module.exports = {
}, },
async channels() { async channels() {
const html = await axios const html = await axios
.get(`https://chaines-tv.orange.fr/programme-tv?filtres=all`) .get('https://chaines-tv.orange.fr/programme-tv?filtres=all')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -40,7 +40,7 @@ module.exports = {
}, },
async channels() { async channels() {
const data = await axios const data = await axios
.get(`https://contenthub-api.eco.astro.com.my/channel/all.json`) .get('https://contenthub-api.eco.astro.com.my/channel/all.json')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
@ -85,7 +85,7 @@ function parseItems(content, date) {
const schedules = data.response.schedule const schedules = data.response.schedule
return schedules[date.format('YYYY-MM-DD')] || [] return schedules[date.format('YYYY-MM-DD')] || []
} catch (e) { } catch {
return [] return []
} }
} }

View file

@ -16,12 +16,13 @@ module.exports = {
}, },
method: 'GET', method: 'GET',
headers: { headers: {
'referer': 'https://www.cosmotetv.gr/', referer: 'https://www.cosmotetv.gr/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'User-Agent':
'Accept': '*/*', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9', 'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept-Encoding': 'gzip, deflate, br, zstd',
'Origin': 'https://www.cosmotetv.gr', Origin: 'https://www.cosmotetv.gr',
'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"', 'Sec-Ch-Ua': '"Not.A/Brand";v="24", "Chromium";v="131", "Google Chrome";v="131"',
'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Ch-Ua-Platform': '"Windows"',
@ -30,12 +31,12 @@ module.exports = {
'Sec-Fetch-Site': 'cross-site' 'Sec-Fetch-Site': 'cross-site'
} }
}, },
url: function ({date, channel}) { url: function ({ date, channel }) {
const startOfDay = dayjs(date).startOf('day').utc().unix() const startOfDay = dayjs(date).startOf('day').utc().unix()
const endOfDay = dayjs(date).endOf('day').utc().unix() const endOfDay = dayjs(date).endOf('day').utc().unix()
return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false` return `https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`
}, },
parser: function ({ date, content }) { parser: function ({ content }) {
let programs = [] let programs = []
const data = JSON.parse(content) const data = JSON.parse(content)
data.channels.forEach(channel => { data.channels.forEach(channel => {
@ -57,16 +58,19 @@ module.exports = {
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
try { try {
const response = await axios.get('https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el', { const response = await axios.get(
'https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/channels/all/el',
{
headers: this.request.headers headers: this.request.headers
}) }
)
const data = response.data const data = response.data
if (data && data.channels) { if (data && data.channels) {
return data.channels.map(item => ({ return data.channels.map(item => ({
lang: 'el', lang: 'el',
site_id: item.callSign, site_id: item.callSign,
name: item.title, name: item.title
//logo: item.logos.square //logo: item.logos.square
})) }))
} else { } else {

View file

@ -1,9 +1,8 @@
const { parser, url, channels } = require('./cosmotetv.gr.config.js') const { parser, url } = require('./cosmotetv.gr.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const axios = require('axios')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
@ -14,34 +13,22 @@ jest.mock('axios')
const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-12-26', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' } const channel = { site_id: 'vouli', xmltv_id: 'HellenicParliamentTV.gr' }
const mockChannelData = {
"channels": [
{
"guid": "XTV100000954",
"title": "ΒΟΥΛΗ HD",
"callSign": "vouli",
"logos": {
"square": "https://tr.static.cdn.cosmotetvott.gr/ote-prod/channel_logos/vouli1-normal.png",
"wide": "https://tr.static.cdn.cosmotetvott.gr/ote-prod/channel_logos/vouli1-wide.png"
}
}
]
}
const mockEpgData = { const mockEpgData = {
"channels": [ channels: [
{ {
"items": [ items: [
{ {
"startTime": "2024-12-26T23:00:00+00:00", startTime: '2024-12-26T23:00:00+00:00',
"endTime": "2024-12-27T00:00:00+00:00", endTime: '2024-12-27T00:00:00+00:00',
"title": "Τι Λέει ο Νόμος", title: 'Τι Λέει ο Νόμος',
"description": "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.", description:
"qoe": { 'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
"genre": "Special" qoe: {
genre: 'Special'
}, },
"thumbnails": { thumbnails: {
"standard": "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg" standard:
'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg'
} }
} }
] ]
@ -52,7 +39,9 @@ const mockEpgData = {
it('can generate valid url', () => { it('can generate valid url', () => {
const startOfDay = dayjs(date).startOf('day').utc().unix() const startOfDay = dayjs(date).startOf('day').utc().unix()
const endOfDay = dayjs(date).endOf('day').utc().unix() const endOfDay = dayjs(date).endOf('day').utc().unix()
expect(url({ date, channel })).toBe(`https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`) expect(url({ date, channel })).toBe(
`https://mwapi-prod.cosmotetvott.gr/api/v3.4/epg/listings/el?from=${startOfDay}&to=${endOfDay}&callSigns=${channel.site_id}&endingIncludedInRange=false`
)
}) })
it('can parse response', () => { it('can parse response', () => {
@ -65,17 +54,23 @@ it('can parse response', () => {
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: "Τι Λέει ο Νόμος", title: 'Τι Λέει ο Νόμος',
description: "νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.", description:
category: "Special", 'νημερωτική εκπομπή. Συζήτηση με τους εισηγητές των κομμάτων για το νομοθετικό έργο.',
image: "https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg", category: 'Special',
start: "2024-12-26T23:00:00.000Z", image:
stop: "2024-12-27T00:00:00.000Z" 'https://gr-ermou-prod-cache05.static.cdn.cosmotetvott.gr/ote-prod/70/280/040029714812000800_1734415727199.jpg',
start: '2024-12-26T23:00:00.000Z',
stop: '2024-12-27T00:00:00.000Z'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ date, channel, content: '{"date":"2024-12-26","categories":[],"channels":[]}' }); const result = parser({
date,
channel,
content: '{"date":"2024-12-26","categories":[],"channels":[]}'
})
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })

View file

@ -9,7 +9,9 @@ module.exports = {
site: 'cubmu.com', site: 'cubmu.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format('YYYY-MM-DD')}&channel_id=${channel.site_id}` return `https://servicebuss.transvision.co.id/v2/cms/getEPGData?app_id=cubmu&tvs_platform_id=standalone&schedule_date=${date.format(
'YYYY-MM-DD'
)}&channel_id=${channel.site_id}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
const programs = [] const programs = []
@ -46,13 +48,19 @@ module.exports = {
} }
} }
// login to service bus // login to service bus
const token = await axios await axios
.post(`https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`, options) .post(
`https://servicebuss.transvision.co.id/tvs/login/external?email=${config.email}&password=${config.password}&deviceId=${config.deviceId}&deviceType=${config.deviceType}&deviceModel=${config.deviceModel}&deviceToken=&serial=&platformId=${config.platformId}`,
options
)
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
// list channels // list channels
const subscribedChannels = await axios const subscribedChannels = await axios
.post(`https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`, options) .post(
`https://servicebuss.transvision.co.id/tvs/subscribe_product/list?platformId=${config.platformId}`,
options
)
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
@ -98,5 +106,9 @@ function parseStart(item) {
} }
function parseStop(item) { function parseStop(item) {
return dayjs.tz([item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '), 'YYYY-MM-DD HH:mm:ss', 'Asia/Jakarta') return dayjs.tz(
[item.schedule_date.split(' ')[0], item.schedule_end_time].join(' '),
'YYYY-MM-DD HH:mm:ss',
'Asia/Jakarta'
)
} }

View file

@ -13,9 +13,9 @@ module.exports = {
site: 'dens.tv', site: 'dens.tv',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format('YYYY-MM-DD')}&id_channel=${ return `https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=${date.format(
channel.site_id 'YYYY-MM-DD'
}&app_type=10` )}&id_channel=${channel.site_id}&app_type=10`
}, },
parser({ content }) { parser({ content }) {
// parsing // parsing
@ -25,8 +25,9 @@ module.exports = {
if (Array.isArray(response?.data)) { if (Array.isArray(response?.data)) {
response.data.forEach(item => { response.data.forEach(item => {
const title = item.title const title = item.title
const [, , , season, , , episode] = title.match(/( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/) || const [, , , season, , , episode] = title.match(
[null, null, null, null, null, null, null] /( (Season |Season|S)(\d+))?( (Episode|Ep) (\d+))/
) || [null, null, null, null, null, null, null]
programs.push({ programs.push({
title, title,
description: item.description, description: item.description,
@ -52,7 +53,7 @@ module.exports = {
const channels = [] const channels = []
for (const id_category of Object.values(categories)) { for (const id_category of Object.values(categories)) {
const data = await axios const data = await axios
.get(`https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory`, { .get('https://www.dens.tv/api/dens3/tv/TvChannels/listByCategory', {
params: { id_category } params: { id_category }
}) })
.then(r => r.data) .then(r => r.data)

View file

@ -10,7 +10,9 @@ const date = dayjs.utc('2024-11-24').startOf('d')
const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' } const channel = { site_id: '38', xmltv_id: 'AniplusAsia.sg', lang: 'id' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10') expect(url({ channel, date })).toBe(
'https://www.dens.tv/api/dens3/tv/TvChannels/listEpgByDate?date=2024-11-24&id_channel=38&app_type=10'
)
}) })
it('can parse response', () => { it('can parse response', () => {

View file

@ -65,7 +65,7 @@ module.exports = {
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://www.digiturk.com.tr/`, { .get('https://www.digiturk.com.tr/', {
headers: { headers: {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'

View file

@ -60,7 +60,7 @@ module.exports = {
$('.pgrid').each((i, el) => { $('.pgrid').each((i, el) => {
const onclick = $(el).find('.chnl-logo').attr('onclick') const onclick = $(el).find('.chnl-logo').attr('onclick')
const number = $(el).find('.cnl-fav > a > span').text().trim() const number = $(el).find('.cnl-fav > a > span').text().trim()
const [, name, site_id] = onclick.match(/ShowChannelGuid\('([^']+)','([^']+)'/) || [ const [, , site_id] = onclick.match(/ShowChannelGuid\('([^']+)','([^']+)'/) || [
null, null,
'', '',
'' ''

View file

@ -49,7 +49,7 @@ module.exports = {
const channels = [] const channels = []
for (let provider of providers) { for (let provider of providers) {
const data = await axios const data = await axios
.post(`https://www.guida.tv/guide/schedule`, null, { .post('https://www.guida.tv/guide/schedule', null, {
params: { params: {
provider, provider,
region: 'Italy', region: 'Italy',
@ -81,7 +81,7 @@ module.exports = {
} }
} }
function parseStart($item, date, channel) { function parseStart($item, date) {
const timeString = $item('td:eq(0)').text().trim() const timeString = $item('td:eq(0)').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${timeString}` const dateString = `${date.format('YYYY-MM-DD')} ${timeString}`

View file

@ -35,7 +35,7 @@ module.exports = {
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://guidatv.sky.it/canali`) .get('https://guidatv.sky.it/canali')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -16,7 +16,7 @@ module.exports = {
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `https://epg-file.hoy.tv/hoy/OTT${channel.site_id}${date.format('YYYYMMDD')}.xml` return `https://epg-file.hoy.tv/hoy/OTT${channel.site_id}${date.format('YYYYMMDD')}.xml`
}, },
parser({ content, channel, date }) { parser({ content, date }) {
const data = convert.xml2js(content, { const data = convert.xml2js(content, {
compact: true, compact: true,
ignoreDeclaration: true, ignoreDeclaration: true,
@ -28,7 +28,7 @@ module.exports = {
for (let item of data.ProgramGuide.Channel.EpgItem) { for (let item of data.ProgramGuide.Channel.EpgItem) {
const start = dayjs.tz(item.EpgStartDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong') const start = dayjs.tz(item.EpgStartDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong')
if (! date.isSame(start, 'day')) { if (!date.isSame(start, 'day')) {
continue continue
} }
@ -40,13 +40,13 @@ module.exports = {
sub_title: subtitle, sub_title: subtitle,
description: item.EpisodeInfo.EpisodeLongDescription._text, description: item.EpisodeInfo.EpisodeLongDescription._text,
start, start,
stop: dayjs.tz(item.EpgEndDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong'), stop: dayjs.tz(item.EpgEndDateTime._text, 'YYYY-MM-DD HH:mm:ss', 'Asia/Hong_Kong')
}) })
} }
return programs return programs
}, },
async channels({ lang }) { async channels() {
const data = await axios const data = await axios
.get('https://api2.hoy.tv/api/v2/a/channel') .get('https://api2.hoy.tv/api/v2/a/channel')
.then(r => r.data) .then(r => r.data)
@ -56,7 +56,7 @@ module.exports = {
return { return {
site_id: c.videos.id, site_id: c.videos.id,
name: c.name.zh_hk, name: c.name.zh_hk,
lang: 'zh', lang: 'zh'
} }
}) })
} }

View file

@ -87,9 +87,7 @@ const content = `<?xml version="1.0" encoding="UTF-8" ?>
</ProgramGuide>` </ProgramGuide>`
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe('https://epg-file.hoy.tv/hoy/OTT7620240913.xml')
'https://epg-file.hoy.tv/hoy/OTT7620240913.xml'
)
}) })
it('can parse response', () => { it('can parse response', () => {
@ -104,13 +102,14 @@ it('can parse response', () => {
start: '2024-09-13T03:30:00.000Z', start: '2024-09-13T03:30:00.000Z',
stop: '2024-09-13T04:30:00.000Z', stop: '2024-09-13T04:30:00.000Z',
title: '點講都係一家人[PG]', title: '點講都係一家人[PG]',
sub_title: '第46集', sub_title: '第46集'
}, },
{ {
start: '2024-09-13T04:30:00.000Z', start: '2024-09-13T04:30:00.000Z',
stop: '2024-09-13T05:30:00.000Z', stop: '2024-09-13T05:30:00.000Z',
title: '麝香之路', title: '麝香之路',
description: 'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world', description:
'Ep. 2 .The Secret of disappeared kingdom.shows the mysterious disappearance of the ancient Tibetan kingdom which gained world'
} }
]) ])
}) })

View file

@ -172,14 +172,17 @@ function parseItems(content, channel, date) {
if (!data || !Array.isArray(data.programs)) return [] if (!data || !Array.isArray(data.programs)) return []
return data.programs return data.programs
.filter(p => p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day)) .filter(
p =>
p.channel === site_id && dayjs(p.start, 'YYYYMMDDHHmmss ZZ').isBetween(curr_day, next_day)
)
.map(p => { .map(p => {
if (Array.isArray(p.date) && p.date.length) { if (Array.isArray(p.date) && p.date.length) {
p.date = p.date[0] p.date = p.date[0]
} }
return p return p
}) })
} catch (error) { } catch {
return [] return []
} }
} }

View file

@ -12,27 +12,30 @@ module.exports = {
site: 'ipko.tv', site: 'ipko.tv',
timezone: 'Europe/Belgrade', timezone: 'Europe/Belgrade',
days: 5, days: 5,
url({ date, channel }) { return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData' }, url() {
return 'https://stargate.ipko.tv/api/titan.tv.WebEpg/GetWebEpgData'
},
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'Host': 'stargate.ipko.tv', Host: 'stargate.ipko.tv',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', 'User-Agent':
'Accept': 'application/json, text/plain, */*', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-AppLayout': '1', 'X-AppLayout': '1',
'x-language': 'sq', 'x-language': 'sq',
'Origin': 'https://ipko.tv', Origin: 'https://ipko.tv',
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site', 'Sec-Fetch-Site': 'cross-site',
'Sec-GPC': '1', 'Sec-GPC': '1',
'Connection': 'keep-alive' Connection: 'keep-alive'
}, },
data({ channel, date }) { data({ channel, date }) {
const todayEpoch = date.startOf('day').unix(); const todayEpoch = date.startOf('day').unix()
const nextDayEpoch = date.add(1, 'day').startOf('day').unix(); const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
return JSON.stringify({ return JSON.stringify({
ch_ext_id: channel.site_id, ch_ext_id: channel.site_id,
from: todayEpoch, from: todayEpoch,
@ -41,11 +44,11 @@ module.exports = {
} }
}, },
parser: function ({ content }) { parser: function ({ content }) {
const programs = []; const programs = []
const data = JSON.parse(content); const data = JSON.parse(content)
data.shows.forEach(show => { data.shows.forEach(show => {
const start = dayjs.unix(show.show_start).utc(); const start = dayjs.unix(show.show_start).utc()
const stop = dayjs.unix(show.show_end).utc(); const stop = dayjs.unix(show.show_end).utc()
const programData = { const programData = {
title: show.title, title: show.title,
description: show.summary || 'No description available', description: show.summary || 'No description available',
@ -58,15 +61,19 @@ module.exports = {
return programs return programs
}, },
async channels() { async channels() {
const response = await axios.post('https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList', JSON.stringify({ includeRadioStations: true }), { const response = await axios.post(
'https://stargate.ipko.tv/api/titan.tv.WebEpg/ZapList',
JSON.stringify({ includeRadioStations: true }),
{
headers: this.request.headers headers: this.request.headers
}); }
)
const data = response.data.data; const data = response.data.data
return data.map(item => ({ return data.map(item => ({
lang: 'sq', lang: 'sq',
name: String(item.channel.title), name: String(item.channel.title),
site_id: String(item.channel.id), site_id: String(item.channel.id)
//logo: String(item.channel.logo) //logo: String(item.channel.logo)
})) }))
} }

View file

@ -76,33 +76,29 @@ it('can parse response', () => {
] ]
}` }`
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel })
p.start = p.start
p.stop = p.stop
return p
})
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: "IPKO Promo", title: 'IPKO Promo',
description: "No description available", description: 'No description available',
start: "2024-12-24T04:00:00.000Z", start: '2024-12-24T04:00:00.000Z',
stop: "2024-12-24T06:00:00.000Z", stop: '2024-12-24T06:00:00.000Z',
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg" thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
}, },
{ {
title: "IPKO Promo", title: 'IPKO Promo',
description: "No description available", description: 'No description available',
start: "2024-12-24T06:00:00.000Z", start: '2024-12-24T06:00:00.000Z',
stop: "2024-12-24T08:00:00.000Z", stop: '2024-12-24T08:00:00.000Z',
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg" thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
}, },
{ {
title: "IPKO Promo", title: 'IPKO Promo',
description: "No description available", description: 'No description available',
start: "2024-12-24T08:00:00.000Z", start: '2024-12-24T08:00:00.000Z',
stop: "2024-12-24T10:00:00.000Z", stop: '2024-12-24T10:00:00.000Z',
thumbnail: "https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg" thumbnail: 'https://vimg.ipko.tv/mtcms/18/2/1/1821cc68-a9bf-4733-b1af-9a5d80163b78.jpg'
} }
]) ])
}) })

View file

@ -38,7 +38,7 @@ module.exports = {
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://m.tv.sms.cz/?zmen_stanice=true`) .get('https://m.tv.sms.cz/?zmen_stanice=true')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -77,8 +77,8 @@ function parseItems(content) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch {
console.log(error.message) return []
} }
if (!data || !Array.isArray(data)) return [] if (!data || !Array.isArray(data)) return []

View file

@ -32,7 +32,7 @@ module.exports = {
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/channels`) .get('https://player.maxtvtogo.tportal.hr:8082/OTT4Proxy/proxy/epg/channels')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -56,7 +56,7 @@ function parseStop($item) {
try { try {
return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ') return dayjs(timeString, 'YYYY-MM-DD HH:mm:ssZZ')
} catch (err) { } catch {
return null return null
} }
} }

View file

@ -10,31 +10,35 @@ dayjs.extend(timezone)
module.exports = { module.exports = {
site: 'mediasetinfinity.mediaset.it', site: 'mediasetinfinity.mediaset.it',
days: 2, days: 2,
url: function ({date, channel}) { url: function ({ date, channel }) {
// Get the epoch timestamp // Get the epoch timestamp
const todayEpoch = date.startOf('day').utc().valueOf() const todayEpoch = date.startOf('day').utc().valueOf()
// Get the epoch timestamp for the next day // Get the epoch timestamp for the next day
const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf() const nextDayEpoch = date.add(1, 'day').startOf('day').utc().valueOf()
return `https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=${todayEpoch}~${nextDayEpoch}&byCallSign=${channel.site_id}` return `https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=${todayEpoch}~${nextDayEpoch}&byCallSign=${channel.site_id}`
}, },
parser: function ({content}) { parser: function ({ content }) {
const programs = [] const programs = []
const data = JSON.parse(content) const data = JSON.parse(content)
if (!data.response || !data.response.entries || !data.response.entries[0] || !data.response.entries[0].listings) { if (
!data.response ||
!data.response.entries ||
!data.response.entries[0] ||
!data.response.entries[0].listings
) {
// If the structure is not as expected, return an empty array // If the structure is not as expected, return an empty array
return programs return programs
} }
const listings = data.response.entries[0].listings const listings = data.response.entries[0].listings
listings.forEach((listing) => { listings.forEach(listing => {
const title = listing.mediasetlisting$epgTitle const title = listing.mediasetlisting$epgTitle
const subTitle = listing.program.title const subTitle = listing.program.title
const season = parseSeason(listing) const season = parseSeason(listing)
const episode = parseEpisode(listing) const episode = parseEpisode(listing)
if (listing.program.title && listing.startTime && listing.endTime) { if (listing.program.title && listing.startTime && listing.endTime) {
programs.push({ programs.push({
title: title || subTitle, title: title || subTitle,
@ -54,7 +58,6 @@ module.exports = {
} }
} }
function parseTime(timestamp) { function parseTime(timestamp) {
return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm') return dayjs(timestamp).utc().format('YYYY-MM-DD HH:mm')
} }
@ -77,17 +80,18 @@ function getMaxResolutionThumbnails(item) {
for (const key in thumbnails) { for (const key in thumbnails) {
const type = key.split('-')[0] // Estrarre il tipo di thumbnail const type = key.split('-')[0] // Estrarre il tipo di thumbnail
const {width, height, url, title} = thumbnails[key] const { width, height, url, title } = thumbnails[key]
if (!maxResolutionThumbnails[type] || if (
(width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height)) { !maxResolutionThumbnails[type] ||
maxResolutionThumbnails[type] = {width, height, url, title} width * height > maxResolutionThumbnails[type].width * maxResolutionThumbnails[type].height
) {
maxResolutionThumbnails[type] = { width, height, url, title }
} }
} }
if (maxResolutionThumbnails.image_keyframe_poster) if (maxResolutionThumbnails.image_keyframe_poster)
return maxResolutionThumbnails.image_keyframe_poster.url return maxResolutionThumbnails.image_keyframe_poster.url
else if (maxResolutionThumbnails.image_header_poster) else if (maxResolutionThumbnails.image_header_poster)
return maxResolutionThumbnails.image_header_poster.url return maxResolutionThumbnails.image_header_poster.url
else else return null
return null
} }

View file

@ -1,4 +1,4 @@
const {parser, url} = require('./mediasetinfinity.mediaset.it.config.js') const { parser, url } = require('./mediasetinfinity.mediaset.it.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')
@ -9,19 +9,24 @@ dayjs.extend(utc)
const date = dayjs.utc('2024-01-20', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-01-20', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'LB', xmltv_id: '20.it' site_id: 'LB',
xmltv_id: '20.it'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ expect(
url({
channel, channel,
date date
})).toBe('https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB') })
).toBe(
'https://api-ott-prod-fe.mediaset.net/PROD/play/feed/allListingFeedEpg/v2.0?byListingTime=1705708800000~1705795200000&byCallSign=LB'
)
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8') const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf8')
const results = parser({content, date}).map(p => { const results = parser({ content, date }).map(p => {
return p return p
}) })
@ -30,11 +35,13 @@ it('can parse response', () => {
stop: '2024-01-20 02:54', stop: '2024-01-20 02:54',
title: 'Chicago Fire', title: 'Chicago Fire',
sub_title: 'Ep. 22 - Io non ti lascio', sub_title: 'Ep. 22 - Io non ti lascio',
description: 'Severide e Kidd continuano a indagare su un vecchio caso doloso di Benny. Notizie inaspettate portano Brett a meditare su una grande decisione.', description:
'Severide e Kidd continuano a indagare su un vecchio caso doloso di Benny. Notizie inaspettate portano Brett a meditare su una grande decisione.',
category: 'Intrattenimento', category: 'Intrattenimento',
season: '7', season: '7',
episode: '22', episode: '22',
image: 'https://static2.mediasetplay.mediaset.it/Mediaset_Italia_Production_-_Main/F309370301002204/media/0/0/1ef76b73-3173-43bd-9c16-73986a0ec131/46896726-11e7-4438-b947-d2ae53f58c0b.jpg' image:
'https://static2.mediasetplay.mediaset.it/Mediaset_Italia_Production_-_Main/F309370301002204/media/0/0/1ef76b73-3173-43bd-9c16-73986a0ec131/46896726-11e7-4438-b947-d2ae53f58c0b.jpg'
}) })
}) })

View file

@ -40,7 +40,7 @@ module.exports = {
async channels() { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.post(`https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getGridAnon`, null, { .post('https://authservice.apps.meo.pt/Services/GridTv/GridTvMng.svc/getGridAnon', null, {
headers: { headers: {
Origin: 'https://www.meo.pt' Origin: 'https://www.meo.pt'
} }

View file

@ -42,7 +42,7 @@ module.exports = {
.catch(console.error) .catch(console.error)
if (content) { if (content) {
const [ $, items ] = getItems(content) const [$, items] = getItems(content)
if (seq === 0) { if (seq === 0) {
queues.push(...items.map(category => baseUrl + $(category).attr('href'))) queues.push(...items.map(category => baseUrl + $(category).attr('href')))
} else { } else {
@ -86,7 +86,11 @@ function parseItems(content, date) {
data.season = parseInt(ep[1]) data.season = parseInt(ep[1])
data.episode = parseInt(ep[2]) data.episode = parseInt(ep[2])
} }
data.start = dayjs.tz(`${lastDate} ${$item.find('.time').text()}`, 'DD/MM/YYYY HH:mm', 'America/Sao_Paulo') data.start = dayjs.tz(
`${lastDate} ${$item.find('.time').text()}`,
'DD/MM/YYYY HH:mm',
'America/Sao_Paulo'
)
result.push(data) result.push(data)
} }
} }

View file

@ -42,7 +42,7 @@ module.exports = {
const axios = require('axios') const axios = require('axios')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://www.mewatch.sg/channel-guide`) .get('https://www.mewatch.sg/channel-guide')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -11,9 +11,7 @@ dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
doFetch doFetch.setCheckResult(false).setDebugger(debug)
.setCheckResult(false)
.setDebugger(debug)
const languages = { en: 'english', id: 'indonesia' } const languages = { en: 'english', id: 'indonesia' }
const cookies = {} const cookies = {}
@ -125,7 +123,7 @@ async function parseItems(content, date, cookies) {
const url = $item.find('a').attr('href') const url = $item.find('a').attr('href')
const headers = { const headers = {
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
Cookie: cookies, Cookie: cookies
} }
queues.push({ i: $item, url, params: { headers, timeout } }) queues.push({ i: $item, url, params: { headers, timeout } })
} }

View file

@ -48,8 +48,10 @@ function parseItems(context) {
schDayPrograms.forEach((program, i) => { schDayPrograms.forEach((program, i) => {
const itemDay = { const itemDay = {
progStart: parseStart($(schDayMonth), $(program)), progStart: parseStart($(schDayMonth), $(program)),
progStop: parseStop($(schDayMonth), schDayPrograms[i + 1] ? progStop: parseStop(
$(schDayPrograms[i + 1]) : null), $(schDayMonth),
schDayPrograms[i + 1] ? $(schDayPrograms[i + 1]) : null
),
progTitle: parseTitle($(program)), progTitle: parseTitle($(program)),
progDesc: parseDescription($(program)) progDesc: parseDescription($(program))
} }
@ -91,7 +93,9 @@ function parseStop(schDayMonth, itemNext) {
) )
} else { } else {
return dayjs.tz( return dayjs.tz(
`${currentYear}-${monthDate[0]}-${(parseInt(monthDate[1]) + 1).toString().padStart(2, '0')} 00:00`, `${currentYear}-${monthDate[0]}-${(parseInt(monthDate[1]) + 1)
.toString()
.padStart(2, '0')} 00:00`,
'YYYY-MMM-DD HH:mm', 'YYYY-MMM-DD HH:mm',
tz tz
) )

File diff suppressed because one or more lines are too long

View file

@ -41,7 +41,7 @@ module.exports = {
const pages = Array.from(Array(totalPages).keys()) const pages = Array.from(Array(totalPages).keys())
for (let page of pages) { for (let page of pages) {
const data = await axios const data = await axios
.get(`https://mtel.ba/oec/epg/program`, { .get('https://mtel.ba/oec/epg/program', {
params: { page, date: dayjs().format('YYYY-MM-DD') }, params: { page, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
@ -65,7 +65,7 @@ module.exports = {
async function getTotalPageCount() { async function getTotalPageCount() {
const data = await axios const data = await axios
.get(`https://mtel.ba/oec/epg/program`, { .get('https://mtel.ba/oec/epg/program', {
params: { page: 0, date: dayjs().format('YYYY-MM-DD') }, params: { page: 0, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'

View file

@ -43,7 +43,7 @@ module.exports = {
const pages = Array.from(Array(totalPages).keys()) const pages = Array.from(Array(totalPages).keys())
for (let page of pages) { for (let page of pages) {
const data = await axios const data = await axios
.get(`https://mts.rs/oec/epg/program`, { .get('https://mts.rs/oec/epg/program', {
params: { page, date: dayjs().format('YYYY-MM-DD') }, params: { page, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
@ -67,7 +67,7 @@ module.exports = {
async function getTotalPageCount() { async function getTotalPageCount() {
const data = await axios const data = await axios
.get(`https://mts.rs/oec/epg/program`, { .get('https://mts.rs/oec/epg/program', {
params: { page: 0, date: dayjs().format('YYYY-MM-DD') }, params: { page: 0, date: dayjs().format('YYYY-MM-DD') },
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
@ -84,8 +84,8 @@ function parseContent(content, channel) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch {
console.log(error) return []
} }
if (!data || !data.channels || !data.channels.length) return null if (!data || !data.channels || !data.channels.length) return null

View file

@ -47,7 +47,7 @@ module.exports = {
const data = await axios const data = await axios
.post( .post(
`https://services.mujtvprogram.cz/tvprogram2services/services/tvchannellist_mobile.php`, 'https://services.mujtvprogram.cz/tvprogram2services/services/tvchannellist_mobile.php',
params, params,
{ {
headers: { headers: {
@ -86,7 +86,7 @@ function parseItems(content) {
if (!data) return [] if (!data) return []
const programmes = data['tv-program-programmes'].programme const programmes = data['tv-program-programmes'].programme
return programmes && Array.isArray(programmes) ? programmes : [] return programmes && Array.isArray(programmes) ? programmes : []
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -9,7 +9,7 @@ dayjs.extend(customParseFormat)
const headers = { const headers = {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0'
} }
module.exports = { module.exports = {

View file

@ -26,8 +26,7 @@ it('can generate valid url for today', () => {
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const results = parser({ content, date }) const results = parser({ content, date }).map(p => {
.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p

View file

@ -23,14 +23,15 @@ module.exports = {
channel.site_id channel.site_id
}.html?dt=${date.format('YYYY-MM-DD')}` }.html?dt=${date.format('YYYY-MM-DD')}`
}, },
async parser({ content, date, channel }) { async parser({ content, date }) {
const programs = [] const programs = []
if (content) { if (content) {
const queues = [] const queues = []
const $ = cheerio.load(content) const $ = cheerio.load(content)
$('table.table > tbody > tr').toArray() $('table.table > tbody > tr')
.toArray()
.forEach(el => { .forEach(el => {
const td = $(el).find('td:eq(1)') const td = $(el).find('td:eq(1)')
const title = td.find('h5 a') const title = td.find('h5 a')
@ -66,12 +67,16 @@ module.exports = {
const subTitle = parseText($('.tab-pane > h5 > strong')) const subTitle = parseText($('.tab-pane > h5 > strong'))
const description = parseText($('.tab-pane > .tvbody > p')) const description = parseText($('.tab-pane > .tvbody > p'))
const image = $('.program-media-image img').attr('src') const image = $('.program-media-image img').attr('src')
const category = $('.schedule-attributes-genres span').toArray() const category = $('.schedule-attributes-genres span')
.toArray()
.map(el => $(el).text()) .map(el => $(el).text())
const casts = $('.single-cast-head:not([id])').toArray() const casts = $('.single-cast-head:not([id])')
.toArray()
.map(el => { .map(el => {
const cast = { name: parseText($(el).find('a')) } const cast = { name: parseText($(el).find('a')) }
const [, role] = $(el).text().match(/\((.*)\)/) || [null, null] const [, role] = $(el)
.text()
.match(/\((.*)\)/) || [null, null]
if (role) { if (role) {
cast.role = role cast.role = role
} }
@ -115,11 +120,17 @@ module.exports = {
// process form -> provider // process form -> provider
if (queue.t === 'p') { if (queue.t === 'p') {
const $ = cheerio.load(res) const $ = cheerio.load(res)
$('#guide_provider option').toArray() $('#guide_provider option')
.toArray()
.forEach(el => { .forEach(el => {
const opt = $(el) const opt = $(el)
const provider = opt.attr('value') const provider = opt.attr('value')
queues.push({ t: 'r', method: 'post', url: 'https://www.mytelly.co.uk/getregions', params: { provider } }) queues.push({
t: 'r',
method: 'post',
url: 'https://www.mytelly.co.uk/getregions',
params: { provider }
})
}) })
} }
// process provider -> region // process provider -> region
@ -135,14 +146,18 @@ module.exports = {
u_time: now.format('HHmm'), u_time: now.format('HHmm'),
is_mobile: 1 is_mobile: 1
} }
queues.push({ t: 's', method: 'post', url: 'https://www.mytelly.co.uk/tv-guide/schedule', params }) queues.push({
t: 's',
method: 'post',
url: 'https://www.mytelly.co.uk/tv-guide/schedule',
params
})
} }
} }
// process schedule -> channels // process schedule -> channels
if (queue.t === 's') { if (queue.t === 's') {
const $ = cheerio.load(res) const $ = cheerio.load(res)
$('.channelname') $('.channelname').each((i, el) => {
.each((i, el) => {
const name = $(el).find('center > a:eq(1)').text() const name = $(el).find('center > a:eq(1)').text()
const url = $(el).find('center > a:eq(1)').attr('href') const url = $(el).find('center > a:eq(1)').attr('href')
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)
@ -178,13 +193,10 @@ function parseTime(date, time) {
} }
function parseText($item) { function parseText($item) {
let text = $item.text() let text = $item.text().replace(/\t/g, '').replace(/\n/g, ' ').trim()
.replace(/\t/g, '')
.replace(/\n/g, ' ')
.trim()
while (true) { while (true) {
if (text.match(/ /)) { if (text.match(/\s\s/)) {
text = text.replace(/ /g, ' ') text = text.replace(/\s\s/g, ' ')
continue continue
} }
break break

View file

@ -17,16 +17,18 @@ const channel = {
xmltv_id: 'BBCOneLondon.uk' xmltv_id: 'BBCOneLondon.uk'
} }
axios.get.mockImplementation((url, opts) => { axios.get.mockImplementation(url => {
if ( if (
url === 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=1906433&tm=2024-12-07+00%3A00%3A00' url ===
'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=1906433&tm=2024-12-07+00%3A00%3A00'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.join(__dirname, '__data__', 'programme.html')) data: fs.readFileSync(path.join(__dirname, '__data__', 'programme.html'))
}) })
} }
if ( if (
url === 'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=5656624&tm=2024-12-07+23%3A35%3A00' url ===
'https://www.mytelly.co.uk/tv-guide/listings/programme?cid=713&pid=5656624&tm=2024-12-07+23%3A35%3A00'
) { ) {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.join(__dirname, '__data__', 'programme2.html')) data: fs.readFileSync(path.join(__dirname, '__data__', 'programme2.html'))
@ -57,7 +59,8 @@ it('can parse response', async () => {
title: 'Captain Phillips', title: 'Captain Phillips',
description: description:
'An American cargo ship sets a dangerous course around the coast of Somalia, while inland, four men are pressed into service as pirates by the local warlords. The captain is taken hostage when the raiding party hijacks the vessel, resulting in a tense five-day crisis. Fact-based thriller, starring Tom Hanks and Barkhad Abdi', 'An American cargo ship sets a dangerous course around the coast of Somalia, while inland, four men are pressed into service as pirates by the local warlords. The captain is taken hostage when the raiding party hijacks the vessel, resulting in a tense five-day crisis. Fact-based thriller, starring Tom Hanks and Barkhad Abdi',
image: 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/c44ce7b0d3ae602c0c93ece5af140815.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4dsylOCGGE7OWlqwSWt0cd0Qtrin4DkEMC0Zzdp8ZeNk2vNIQzjMF0DG0h3IeTR5NM%3D', image:
'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/c44ce7b0d3ae602c0c93ece5af140815.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4dsylOCGGE7OWlqwSWt0cd0Qtrin4DkEMC0Zzdp8ZeNk2vNIQzjMF0DG0h3IeTR5NM%3D',
category: ['Factual', 'Movie/Drama', 'Thriller'] category: ['Factual', 'Movie/Drama', 'Thriller']
}) })
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
@ -67,7 +70,8 @@ it('can parse response', async () => {
subTitle: 'Past and Pressure Season 6, Episode 5', subTitle: 'Past and Pressure Season 6, Episode 5',
description: description:
'The artists are tasked with writing a song about their heritage. For some, the pressure of the competition proves too much for them to match. In their final challenge, they are put face to face with industry experts who grill them about their plans after the competition. Some impress, while others leave the mentors confused', 'The artists are tasked with writing a song about their heritage. For some, the pressure of the competition proves too much for them to match. In their final challenge, they are put face to face with industry experts who grill them about their plans after the competition. Some impress, while others leave the mentors confused',
image: 'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/2039278182b27cc279570b9ab9b89379.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4cDhR7jXTNFW3tgwQCdOPUobhXwlT81mIsqOe93HPusDG6tw1aoeYOgafojtynNWxc%3D', image:
'https://d16ia5iwuvax6y.cloudfront.net/uk-prog-images/2039278182b27cc279570b9ab9b89379.jpg?k=VeeNdUjml3bSHdlZ0OXbGLy%2BmsLdYPwTV6iAxGkzq4cDhR7jXTNFW3tgwQCdOPUobhXwlT81mIsqOe93HPusDG6tw1aoeYOgafojtynNWxc%3D',
category: ['Challenge/Reality Show', 'Show/Game Show'], category: ['Challenge/Reality Show', 'Show/Game Show'],
season: 6, season: 6,
episode: 5 episode: 5

View file

@ -12,27 +12,30 @@ module.exports = {
site: 'neo.io', site: 'neo.io',
timezone: 'Europe/Ljubljana', timezone: 'Europe/Ljubljana',
days: 5, days: 5,
url({ date, channel }) { return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData' }, url() {
return 'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
},
request: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'Host': 'stargate.telekom.si', Host: 'stargate.telekom.si',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', 'User-Agent':
'Accept': 'application/json, text/plain, */*', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3', 'Accept-Language': 'nl,en-US;q=0.7,en;q=0.3',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-AppLayout': '1', 'X-AppLayout': '1',
'x-language': 'sl', 'x-language': 'sl',
'Origin': 'https://neo.io', Origin: 'https://neo.io',
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site', 'Sec-Fetch-Site': 'cross-site',
'Sec-GPC': '1', 'Sec-GPC': '1',
'Connection': 'keep-alive' Connection: 'keep-alive'
}, },
data({ channel, date }) { data({ channel, date }) {
const todayEpoch = date.startOf('day').unix(); const todayEpoch = date.startOf('day').unix()
const nextDayEpoch = date.add(1, 'day').startOf('day').unix(); const nextDayEpoch = date.add(1, 'day').startOf('day').unix()
return JSON.stringify({ return JSON.stringify({
ch_ext_id: channel.site_id, ch_ext_id: channel.site_id,
from: todayEpoch, from: todayEpoch,
@ -41,11 +44,11 @@ module.exports = {
} }
}, },
parser: function ({ content }) { parser: function ({ content }) {
const programs = []; const programs = []
const data = JSON.parse(content); const data = JSON.parse(content)
data.shows.forEach(show => { data.shows.forEach(show => {
const start = dayjs.unix(show.show_start).utc(); const start = dayjs.unix(show.show_start).utc()
const stop = dayjs.unix(show.show_end).utc(); const stop = dayjs.unix(show.show_end).utc()
const programData = { const programData = {
title: show.title, title: show.title,
description: show.summary || 'No description available', description: show.summary || 'No description available',
@ -58,15 +61,19 @@ module.exports = {
return programs return programs
}, },
async channels() { async channels() {
const response = await axios.post('https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList', JSON.stringify({ includeRadioStations: true }), { const response = await axios.post(
'https://stargate.telekom.si/api/titan.tv.WebEpg/ZapList',
JSON.stringify({ includeRadioStations: true }),
{
headers: this.request.headers headers: this.request.headers
}); }
)
const data = response.data.data; const data = response.data.data
return data.map(item => ({ return data.map(item => ({
lang: 'sq', lang: 'sq',
name: String(item.channel.title), name: String(item.channel.title),
site_id: String(item.channel.id), site_id: String(item.channel.id)
//logo: String(item.channel.logo) //logo: String(item.channel.logo)
})) }))
} }

View file

@ -12,7 +12,9 @@ const channel = {
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date, channel })).toBe('https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData') expect(url({ date, channel })).toBe(
'https://stargate.telekom.si/api/titan.tv.WebEpg/GetWebEpgData'
)
}) })
it('can parse response', () => { it('can parse response', () => {
@ -82,33 +84,34 @@ it('can parse response', () => {
] ]
}` }`
const result = parser({ content, channel }).map(p => { const result = parser({ content, channel })
p.start = p.start
p.stop = p.stop
return p
})
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: "Napovedujemo", title: 'Napovedujemo',
description: "Vabilo k ogledu naših oddaj.", description: 'Vabilo k ogledu naših oddaj.',
start: "2024-12-26T04:05:00.000Z", start: '2024-12-26T04:05:00.000Z',
stop: "2024-12-26T05:50:00.000Z", stop: '2024-12-26T05:50:00.000Z',
thumbnail: "https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg" thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/52/0/0/5200d01a-fe5f-487e-835a-274e77227a6b.jpg'
}, },
{ {
title: "S0E0 - Hrabri zajčki: Prvi sneg", title: 'S0E0 - Hrabri zajčki: Prvi sneg',
description: "Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.", description:
start: "2024-12-26T05:50:00.000Z", 'Hrabri zajčki so prispeli v borov gozd in izkusili prvi sneg. Bob in Bu še nikoli nista videla snega. Mami kuha korenčkov kakav, Bu in Bob pa kmalu spoznata novega prijatelja, losa Danija.',
stop: "2024-12-26T06:00:00.000Z", start: '2024-12-26T05:50:00.000Z',
thumbnail: "https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg" stop: '2024-12-26T06:00:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/d6/4/5/d6456f4a-4f0a-4825-90c1-1749abd59688.jpg'
}, },
{ {
title: "Dobro jutro", title: 'Dobro jutro',
description: "Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.", description:
start: "2024-12-26T06:00:00.000Z", 'Oddaja Dobro jutro poleg informativnih in zabavnih vsebin podaja koristne nasvete o najrazličnejših tematikah iz vsakdanjega življenja.',
stop: "2024-12-26T09:05:00.000Z", start: '2024-12-26T06:00:00.000Z',
thumbnail: "https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg" stop: '2024-12-26T09:05:00.000Z',
thumbnail:
'https://ngimg.siol.tv/sioltv/mtcmsprod/e1/2/d/e12d8eb4-693a-43d3-89d4-fd96dade9f0f.jpg'
} }
]) ])
}) })

View file

@ -50,7 +50,7 @@ function parseItems(content, date) {
if (!data || !data.item || !Array.isArray(data.item.episodes)) return [] if (!data || !data.item || !Array.isArray(data.item.episodes)) return []
return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD'))) return data.item.episodes.filter(ep => ep.schedule.startsWith(date.format('YYYY-MM-DD')))
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -5,7 +5,8 @@ module.exports = {
// I'm not sure what `endDate` represents but they only return 1 day of // I'm not sure what `endDate` represents but they only return 1 day of
// results, with `endTime`s ocassionally in the following day. // results, with `endTime`s ocassionally in the following day.
days: 1, days: 1,
url: ({ date }) => `https://api-web.nhle.com/v1/network/tv-schedule/${date.toJSON().split("T")[0]}`, url: ({ date }) =>
`https://api-web.nhle.com/v1/network/tv-schedule/${date.toJSON().split('T')[0]}`,
parser({ content }) { parser({ content }) {
const programs = [] const programs = []
const items = parseItems(content) const items = parseItems(content)
@ -13,7 +14,7 @@ module.exports = {
programs.push({ programs.push({
title: item.title, title: item.title,
description: item.description === item.title ? undefined : item.description, description: item.description === item.title ? undefined : item.description,
category: "Sports", category: 'Sports',
// image: parseImage(item), // image: parseImage(item),
start: parseStart(item), start: parseStart(item),
stop: parseStop(item) stop: parseStop(item)

View file

@ -10,9 +10,7 @@ dayjs.extend(utc)
const date = dayjs.utc('2024-11-21', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2024-11-21', 'YYYY-MM-DD').startOf('d')
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe( expect(url({ date })).toBe('https://api-web.nhle.com/v1/network/tv-schedule/2024-11-21')
'https://api-web.nhle.com/v1/network/tv-schedule/2024-11-21'
)
}) })
it('can parse response', () => { it('can parse response', () => {
@ -28,17 +26,19 @@ it('can parse response', () => {
start: '2024-11-21T12:00:00.000Z', start: '2024-11-21T12:00:00.000Z',
stop: '2024-11-21T13:00:00.000Z', stop: '2024-11-21T13:00:00.000Z',
title: 'On The Fly', title: 'On The Fly',
category: 'Sports', category: 'Sports'
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const results = parser({ content: JSON.stringify({ const results = parser({
content: JSON.stringify({
// extra props not necessary but they form a valid response // extra props not necessary but they form a valid response
date: "2024-11-21", date: '2024-11-21',
startDate: "2024-11-07", startDate: '2024-11-07',
endDate: "2024-12-05", endDate: '2024-12-05',
broadcasts: [], broadcasts: []
}) }) })
})
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

@ -55,7 +55,7 @@ module.exports = {
.map(function () { .map(function () {
return { return {
lang: 'es', lang: 'es',
site_id: $(this).attr('alt').replace(/\&/gi, '&amp;'), site_id: $(this).attr('alt').replace(/&/gi, '&amp;'),
name: $(this).attr('alt') name: $(this).attr('alt')
} }
}) })

View file

@ -27,7 +27,7 @@ module.exports = {
if (item.episodeNum) { if (item.episodeNum) {
item.episodeNum.forEach(ep => { item.episodeNum.forEach(ep => {
if (ep.system === 'xmltv_ns') { if (ep.system === 'xmltv_ns') {
const [season, episode, _] = ep.value.split('.') const [season, episode] = ep.value.split('.')
program.season = parseInt(season) + 1 program.season = parseInt(season) + 1
program.episode = parseInt(episode) + 1 program.episode = parseInt(episode) + 1
return true return true

View file

@ -132,7 +132,7 @@ module.exports = {
const $ = cheerio.load(data) const $ = cheerio.load(data)
$('.channelname').each((i, el) => { $('.channelname').each((i, el) => {
let name = $(el).find('center > a:eq(1)').text() let name = $(el).find('center > a:eq(1)').text()
name = name.replace(/\-\-/gi, '-') name = name.replace(/--/gi, '-')
const url = $(el).find('center > a:eq(1)').attr('href') const url = $(el).find('center > a:eq(1)').attr('href')
if (!url) return if (!url) return
const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/) const [, number, slug] = url.match(/\/(\d+)\/(.*)\.html$/)

View file

@ -5,7 +5,8 @@ const axios = require('axios')
dayjs.extend(utc) dayjs.extend(utc)
const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO' const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO'
const API_CHANNEL_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json' const API_CHANNEL_ENDPOINT =
'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json'
const API_IMAGE_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images' const API_IMAGE_ENDPOINT = 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images'
module.exports = { module.exports = {
@ -25,15 +26,9 @@ module.exports = {
if (!items.length) return programs if (!items.length) return programs
const promises = [ const promises = [
axios.get( axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json`),
`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json`, axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_2.json`),
), axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_3.json`)
axios.get(
`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_2.json`,
),
axios.get(
`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_3.json`,
),
] ]
await Promise.allSettled(promises) await Promise.allSettled(promises)
@ -42,7 +37,9 @@ module.exports = {
if (r.status === 'fulfilled') { if (r.status === 'fulfilled') {
const parsed = parseItems(r.value.data, channel) const parsed = parseItems(r.value.data, channel)
items = items.filter((item, index) => items.findIndex(oi => oi.id === item.id) === index).concat(parsed) items = items
.filter((item, index) => items.findIndex(oi => oi.id === item.id) === index)
.concat(parsed)
} }
}) })
}) })
@ -57,7 +54,7 @@ module.exports = {
episode: item.episodeId || null, episode: item.episodeId || null,
icon: parseIcon(item), icon: parseIcon(item),
start: dayjs.utc(item.startDate) || null, start: dayjs.utc(item.startDate) || null,
stop: dayjs.utc(item.endDate) || null, stop: dayjs.utc(item.endDate) || null
}) })
}) })
@ -79,35 +76,33 @@ module.exports = {
} }
} }
function parseIcon(item){ function parseIcon(item) {
if (item.attachments.length > 0) {
const cover = item.attachments.find(i => i.name === 'COVER' || i.name === 'cover')
if(item.attachments.length > 0){ if (cover) {
const cover = item.attachments.find(i => i.name === "COVER" || i.name === "cover") return `${API_IMAGE_ENDPOINT}${cover.value}`
if(cover)
{
return `${API_IMAGE_ENDPOINT}${cover.value}`;
} }
} }
return '' return ''
} }
function parseGenres(item){ function parseGenres(item) {
return item.genres.map(i => i.name); return item.genres.map(i => i.name)
} }
function parseItems(content, channel) { function parseItems(content, channel) {
const json = typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : [] const json =
typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : []
if (!Array.isArray(json)) { if (!Array.isArray(json)) {
return []; return []
} }
const channelData = json.find(i => i.channelExternalId == channel.site_id); const channelData = json.find(i => i.channelExternalId == channel.site_id)
if(!channelData) if (!channelData) return []
return [];
return channelData.programs; return channelData.programs
} }

View file

@ -14,7 +14,11 @@ const channel = {
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe(`https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/${date.format('YYYYMMDD')}_8h_1.json`) expect(url({ date })).toBe(
`https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/${date.format(
'YYYYMMDD'
)}_8h_1.json`
)
}) })
it('can parse response', async () => { it('can parse response', async () => {
@ -28,13 +32,14 @@ it('can parse response', async () => {
expect(results.length).toBe(4) expect(results.length).toBe(4)
var sampleResult = results[0]; var sampleResult = results[0]
expect(sampleResult).toMatchObject({ expect(sampleResult).toMatchObject({
start: '2024-11-30T22:36:51.000Z', start: '2024-11-30T22:36:51.000Z',
stop: '2024-11-30T23:57:25.000Z', stop: '2024-11-30T23:57:25.000Z',
category: ['Cine', 'Romance', 'Comedia', 'Comedia Romántica'], category: ['Cine', 'Romance', 'Comedia', 'Comedia Romántica'],
description: 'Charlie trabaja como director en una escuela de primaria y goza de una placentera existencia junto a sus amigos. A pesar de ello, no es feliz porque cada vez que se enamora pierde la cordura.', description:
'Charlie trabaja como director en una escuela de primaria y goza de una placentera existencia junto a sus amigos. A pesar de ello, no es feliz porque cada vez que se enamora pierde la cordura.',
title: 'Loco de amor' title: 'Loco de amor'
}) })
}) })

View file

@ -5,7 +5,12 @@ const timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const packages = { 'OSNTV CONNECT': 3720, 'OSNTV PRIME': 3733, 'ALFA': 1281, 'OSN PINOY PLUS EXTRA': 3519 } const packages = {
'OSNTV CONNECT': 3720,
'OSNTV PRIME': 3733,
ALFA: 1281,
'OSN PINOY PLUS EXTRA': 3519
}
const country = 'AE' const country = 'AE'
const tz = 'Asia/Dubai' const tz = 'Asia/Dubai'
@ -13,11 +18,9 @@ module.exports = {
site: 'osn.com', site: 'osn.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${ return `https://www.osn.com/api/TVScheduleWebService.asmx/time?dt=${encodeURIComponent(
encodeURIComponent(date.format('MM/DD/YYYY')) date.format('MM/DD/YYYY')
}&co=${country}&ch=${ )}&co=${country}&ch=${channel.site_id}&mo=false&hr=0`
channel.site_id
}&mo=false&hr=0`
}, },
request: { request: {
headers({ channel }) { headers({ channel }) {
@ -46,7 +49,9 @@ module.exports = {
const axios = require('axios') const axios = require('axios')
for (const pkg of Object.values(packages)) { for (const pkg of Object.values(packages)) {
const channels = await axios const channels = await axios
.get(`https://www.osn.com/api/tvchannels.ashx?culture=en-US&packageId=${pkg}&country=${country}`) .get(
`https://www.osn.com/api/tvchannels.ashx?culture=en-US&packageId=${pkg}&country=${country}`
)
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)

View file

@ -28,8 +28,7 @@ it('can generate valid url', () => {
}) })
it('can parse response (ar)', () => { it('can parse response (ar)', () => {
const result = parser({ date, channel: channelAR, content }) const result = parser({ date, channel: channelAR, content }).map(a => {
.map(a => {
a.start = a.start.toJSON() a.start = a.start.toJSON()
a.stop = a.stop.toJSON() a.stop = a.stop.toJSON()
return a return a
@ -38,13 +37,12 @@ it('can parse response (ar)', () => {
expect(result[1]).toMatchObject({ expect(result[1]).toMatchObject({
start: '2024-11-26T20:50:00.000Z', start: '2024-11-26T20:50:00.000Z',
stop: '2024-11-26T21:45:00.000Z', stop: '2024-11-26T21:45:00.000Z',
title: 'بيت الحلويات: الحلقة 3', title: 'بيت الحلويات: الحلقة 3'
}) })
}) })
it('can parse response (en)', () => { it('can parse response (en)', () => {
const result = parser({ date, channel: channelEN, content }) const result = parser({ date, channel: channelEN, content }).map(a => {
.map(a => {
a.start = a.start.toJSON() a.start = a.start.toJSON()
a.stop = a.stop.toJSON() a.stop = a.stop.toJSON()
return a return a
@ -53,7 +51,7 @@ it('can parse response (en)', () => {
expect(result[1]).toMatchObject({ expect(result[1]).toMatchObject({
start: '2024-11-26T20:50:00.000Z', start: '2024-11-26T20:50:00.000Z',
stop: '2024-11-26T21:45:00.000Z', stop: '2024-11-26T21:45:00.000Z',
title: 'House Of Desserts: Episode 3', title: 'House Of Desserts: Episode 3'
}) })
}) })

View file

@ -27,7 +27,7 @@ function parseItems(content, date) {
let data let data
try { try {
data = JSON.parse(json) data = JSON.parse(json)
} catch (error) { } catch {
return [] return []
} }

View file

@ -129,16 +129,13 @@ module.exports = {
) )
} }
} }
function fetchApiVersion() { async function fetchApiVersion() {
return new Promise(async (resolve, reject) => {
try {
// you'll never find what happened here :) // you'll never find what happened here :)
// load the pickx page and get the hash from the MWC configuration. // load the pickx page and get the hash from the MWC configuration.
// it's not the best way to get the version but it's the only way to get it. // it's not the best way to get the version but it's the only way to get it.
const hashUrl = 'https://www.pickx.be/nl/televisie/tv-gids'
const hashUrl = 'https://www.pickx.be/nl/televisie/tv-gids'; const hashData = await axios
.get(hashUrl)
const hashData = await axios.get(hashUrl)
.then(r => { .then(r => {
const re = /"hashes":\["(.*)"\]/ const re = /"hashes":\["(.*)"\]/
const match = r.data.match(re) const match = r.data.match(re)
@ -148,10 +145,9 @@ function fetchApiVersion() {
throw new Error('React app version hash not found') throw new Error('React app version hash not found')
} }
}) })
.catch(console.error); .catch(console.error)
const versionUrl = `https://www.pickx.be/api/s-${hashData}` const versionUrl = `https://www.pickx.be/api/s-${hashData}`
const response = await axios.get(versionUrl, { const response = await axios.get(versionUrl, {
headers: { headers: {
Origin: 'https://www.pickx.be', Origin: 'https://www.pickx.be',
@ -159,6 +155,8 @@ function fetchApiVersion() {
} }
}) })
return new Promise((resolve, reject) => {
try {
if (response.status === 200) { if (response.status === 200) {
apiVersion = response.data.version apiVersion = response.data.version
resolve() resolve()

View file

@ -6,14 +6,7 @@ jest.mock('./pickx.be.config.js', () => {
} }
}) })
const { const { parser, url, request, setApiVersion } = require('./pickx.be.config.js')
parser,
url,
request,
fetchApiVersion,
setApiVersion,
getApiVersion
} = require('./pickx.be.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
@ -36,7 +29,7 @@ beforeEach(() => {
it('can generate valid url', async () => { it('can generate valid url', async () => {
const generatedUrl = await url({ channel, date }) const generatedUrl = await url({ channel, date })
expect(generatedUrl).toBe( expect(generatedUrl).toBe(
`https://px-epg.azureedge.net/airings/mockedApiVersion/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels` 'https://px-epg.azureedge.net/airings/mockedApiVersion/2023-12-13/channel/UID0118?timezone=Europe%2FBrussels'
) )
}) })

View file

@ -8,9 +8,9 @@ module.exports = {
site: 'player.ee.co.uk', site: 'player.ee.co.uk',
days: 2, days: 2,
url({ date, channel, hour = 0 }) { url({ date, channel, hour = 0 }) {
return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${ return `https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=${encodeURIComponent(
encodeURIComponent(channel.site_id) channel.site_id
}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2,'0')}Z/PT12H` )}&interval=${date.format('YYYY-MM-DD')}T${hour.toString().padStart(2, '0')}Z/PT12H`
}, },
request: { request: {
headers: { headers: {
@ -39,7 +39,7 @@ module.exports = {
const stop = start.add(item.publishedDuration, 's') const stop = start.add(item.publishedDuration, 's')
const description = item.synopsis const description = item.synopsis
if (description) { if (description) {
const matches = description.trim().match(/\(?S(\d+)[\/\s]Ep(\d+)\)?/) const matches = description.trim().match(/\(?S(\d+)[/\s]Ep(\d+)\)?/)
if (matches) { if (matches) {
if (matches[1]) { if (matches[1]) {
season = parseInt(matches[1]) season = parseInt(matches[1])
@ -79,7 +79,7 @@ module.exports = {
'BTSubscriptionCodesExtension' 'BTSubscriptionCodesExtension'
] ]
const result = await axios const result = await axios
.get(`https://api.youview.tv/metadata/linear/v2/linear-services`, { .get('https://api.youview.tv/metadata/linear/v2/linear-services', {
params: { params: {
contentTargetingToken: token, contentTargetingToken: token,
extensions: extensions.join(',') extensions: extensions.join(',')
@ -89,7 +89,8 @@ module.exports = {
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
return result?.items return (
result?.items
.filter(channel => channel.contentTypes.indexOf('tv') >= 0) .filter(channel => channel.contentTypes.indexOf('tv') >= 0)
.map(channel => { .map(channel => {
return { return {
@ -98,5 +99,6 @@ module.exports = {
name: channel.fullName name: channel.fullName
} }
}) || [] }) || []
)
} }
} }

View file

@ -15,8 +15,11 @@ const channel = {
xmltv_id: 'HGTV.uk' xmltv_id: 'HGTV.uk'
} }
axios.get.mockImplementation((url, opts) => { axios.get.mockImplementation(url => {
if (url === 'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T12Z/PT12H') { if (
url ===
'https://api.youview.tv/metadata/linear/v2/schedule/by-servicelocator?serviceLocator=dvb%3A%2F%2F233a..6d60&interval=2023-12-13T12Z/PT12H'
) {
return Promise.resolve({ return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/data1.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/data1.json')))
}) })
@ -33,8 +36,7 @@ it('can generate valid url', () => {
it('can parse response', async () => { it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json'))
const result = (await parser({ content, channel, date })) const result = (await parser({ content, channel, date })).map(p => {
.map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p

View file

@ -43,7 +43,7 @@ module.exports = {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.post( .post(
`https://playtv.unifi.com.my:7053/VSP/V3/QueryAllChannel`, 'https://playtv.unifi.com.my:7053/VSP/V3/QueryAllChannel',
{ isReturnAllMedia: '0' }, { isReturnAllMedia: '0' },
{ {
params: { params: {
@ -74,7 +74,7 @@ function parseItems(content, channel) {
const channelData = data.find(i => i.id == channel.site_id) const channelData = data.find(i => i.id == channel.site_id)
return channelData.items && Array.isArray(channelData.items) ? channelData.items : [] return channelData.items && Array.isArray(channelData.items) ? channelData.items : []
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -46,10 +46,10 @@ module.exports = {
return programs return programs
}, },
async channels({ country, lang }) { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://www.programetv.ro/api/station/index/`) .get('https://www.programetv.ro/api/station/index/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -62,7 +62,7 @@ module.exports = {
$('.channelList-listItemsLink').each((i, el) => { $('.channelList-listItemsLink').each((i, el) => {
const name = $(el).attr('title') const name = $(el).attr('title')
const url = $(el).attr('href') const url = $(el).attr('href')
const [, site_id] = url.match(/\/programme\-(.*)\.html$/i) const [, site_id] = url.match(/\/programme-(.*)\.html$/i)
channels.push({ channels.push({
lang: 'fr', lang: 'fr',

View file

@ -51,7 +51,7 @@ module.exports = {
return data.programmes.map(item => { return data.programmes.map(item => {
const site_id = item.url.replace('/', '') const site_id = item.url.replace('/', '')
const name = site_id.replace(/\-/gi, ' ') const name = site_id.replace(/-/gi, ' ')
return { return {
lang: 'fr', lang: 'fr',

View file

@ -13,19 +13,17 @@ module.exports = {
site: 'programme.tvb.com', site: 'programme.tvb.com',
days: 2, days: 2,
url({ channel, date, time = null }) { url({ channel, date, time = null }) {
return `https://programme.tvb.com/api/schedule?input_date=${ return `https://programme.tvb.com/api/schedule?input_date=${date.format(
date.format('YYYYMMDD') 'YYYYMMDD'
}&network_code=${channel.site_id}&_t=${time ? time : parseInt(Date.now() / 1000)}` )}&network_code=${channel.site_id}&_t=${time ? time : parseInt(Date.now() / 1000)}`
}, },
parser({ content, channel, date }) { parser({ content, channel, date }) {
const programs = [] const programs = []
const data = content ? JSON.parse(content) : {} const data = content ? JSON.parse(content) : {}
if (Array.isArray(data.data?.list)) { if (Array.isArray(data.data?.list)) {
const dt = date.format('YYYY-MM-DD')
for (const d of data.data.list) { for (const d of data.data.list) {
if (Array.isArray(d.schedules)) { if (Array.isArray(d.schedules)) {
const schedules = d.schedules const schedules = d.schedules.filter(s => s.network_code === channel.site_id)
.filter(s => s.network_code === channel.site_id)
schedules.forEach((s, i) => { schedules.forEach((s, i) => {
const start = dayjs.tz(s.event_datetime, 'YYYY-MM-DD HH:mm:ss', tz) const start = dayjs.tz(s.event_datetime, 'YYYY-MM-DD HH:mm:ss', tz)
let stop let stop
@ -64,7 +62,7 @@ module.exports = {
if (assets) { if (assets) {
queues.push(...assets.map(a => base + '/' + a)) queues.push(...assets.map(a => base + '/' + a))
} else { } else {
const metadata = content.match(/e\=(\[(.*?)\])/) const metadata = content.match(/e=(\[(.*?)\])/)
if (metadata) { if (metadata) {
const infos = eval(metadata[1]) const infos = eval(metadata[1])
if (Array.isArray(infos)) { if (Array.isArray(infos)) {

View file

@ -34,7 +34,7 @@ it('can parse response (en)', () => {
expect(results[1]).toMatchObject({ expect(results[1]).toMatchObject({
start: '2024-12-06T15:55:00.000Z', start: '2024-12-06T15:55:00.000Z',
stop: '2024-12-06T16:55:00.000Z', stop: '2024-12-06T16:55:00.000Z',
title: 'Line Walker: Bull Fight#16[Can][PG]', title: 'Line Walker: Bull Fight#16[Can][PG]'
}) })
}) })
@ -51,7 +51,7 @@ it('can parse response (zh)', () => {
stop: '2024-12-06T16:55:00.000Z', stop: '2024-12-06T16:55:00.000Z',
title: '使徒行者3#16[粵][PG]', title: '使徒行者3#16[粵][PG]',
description: description:
'文鼎從淑梅手上救走大聖爺兒子,大聖爺還恩於歡喜,答允支持九指強。崇聯社定下選舉日子,恰巧是韋傑出獄之日,頭目們顧念舊日恩義,紛紛轉投浩洋。浩洋帶亞希逛傢俬店,憧憬二人未來。亞希向家強承認愛上浩洋,要求退出臥底任務。作榮與歡喜暗中會面,將國際犯罪組織「永恆幫」情報交給他。阿火遭家強出賣,到沐足店搶錢。家強逮住阿火,惟被合星誤會而受拘捕。家強把正植遺下的頸鏈和學生證交還,合星意識到家強已知悉正植身世。', '文鼎從淑梅手上救走大聖爺兒子,大聖爺還恩於歡喜,答允支持九指強。崇聯社定下選舉日子,恰巧是韋傑出獄之日,頭目們顧念舊日恩義,紛紛轉投浩洋。浩洋帶亞希逛傢俬店,憧憬二人未來。亞希向家強承認愛上浩洋,要求退出臥底任務。作榮與歡喜暗中會面,將國際犯罪組織「永恆幫」情報交給他。阿火遭家強出賣,到沐足店搶錢。家強逮住阿火,惟被合星誤會而受拘捕。家強把正植遺下的頸鏈和學生證交還,合星意識到家強已知悉正植身世。'
}) })
}) })

View file

@ -50,7 +50,7 @@ module.exports = {
$('ul.channelList a').each((i, el) => { $('ul.channelList a').each((i, el) => {
const name = $(el).text() const name = $(el).text()
const url = $(el).attr('href') const url = $(el).attr('href')
const [, site_id] = url.match(/^\/program\-tv\/(.*)$/i) const [, site_id] = url.match(/^\/program-tv\/(.*)$/i)
channels.push({ channels.push({
lang: 'pl', lang: 'pl',

View file

@ -58,7 +58,7 @@ function parseItems(content, channel) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch {
return [] return []
} }

View file

@ -11,9 +11,7 @@ dayjs.extend(timezone)
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
doFetch doFetch.setCheckResult(false).setDebugger(debug)
.setCheckResult(false)
.setDebugger(debug)
const tz = 'Asia/Riyadh' const tz = 'Asia/Riyadh'
const defaultHeaders = { const defaultHeaders = {
@ -47,7 +45,7 @@ module.exports = {
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
cookie: cookies[channel.lang], cookie: cookies[channel.lang]
} }
} }
queues.push({ i: item, url, params }) queues.push({ i: item, url, params })
@ -61,7 +59,7 @@ module.exports = {
}, },
async channels({ lang = 'en' }) { async channels({ lang = 'en' }) {
const result = await axios const result = await axios
.get(`https://rotana.net/api/channels`) .get('https://rotana.net/api/channels')
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
@ -88,34 +86,37 @@ function parseProgram(item, result) {
item.description = desc item.description = desc
} }
} }
break; break
case 'Element': case 'Element':
if (el.name === 'span') { if (el.name === 'span') {
const [k, v] = $(el).text().split(':').map(a => a.trim()) const [k, v] = $(el)
.text()
.split(':')
.map(a => a.trim())
switch (k) { switch (k) {
case 'Category': case 'Category':
case 'التصنيف': case 'التصنيف':
item.category = v; item.category = v
break; break
case 'Country': case 'Country':
case 'البلد': case 'البلد':
item.country = v; item.country = v
break; break
case 'Director': case 'Director':
case 'المخرج': case 'المخرج':
item.director = v; item.director = v
break; break
case 'Language': case 'Language':
case 'اللغة': case 'اللغة':
item.language = v; item.language = v
break; break
case 'Release Year': case 'Release Year':
case 'سنة الإصدار': case 'سنة الإصدار':
item.date = v; item.date = v
break; break
} }
} }
break; break
} }
} }
} }
@ -142,7 +143,9 @@ function parseItems(content, date) {
const heading = top.find('.iq-accordion-title .big-title') const heading = top.find('.iq-accordion-title .big-title')
if (heading.length) { if (heading.length) {
const progId = top.attr('id') const progId = top.attr('id')
const title = heading.find('span:eq(1)').text() const title = heading
.find('span:eq(1)')
.text()
.split('\n') .split('\n')
.map(a => a.trim()) .map(a => a.trim())
.join(' ') .join(' ')
@ -151,7 +154,7 @@ function parseItems(content, date) {
items.push({ items.push({
program: progId.substr(progId.indexOf('-') + 1), program: progId.substr(progId.indexOf('-') + 1),
title: title ? title.trim() : title, title: title ? title.trim() : title,
start: `${y}-${m}-${d} ${time.trim()}`, start: `${y}-${m}-${d} ${time.trim()}`
}) })
} }
} }

View file

@ -19,7 +19,7 @@ const channel = {
} }
const channelAr = Object.assign({}, channel, { lang: 'ar' }) const channelAr = Object.assign({}, channel, { lang: 'ar' })
axios.get.mockImplementation((url, opts) => { axios.get.mockImplementation(url => {
if (url === 'https://rotana.net/en/streams?channel=439&itemId=736970') { if (url === 'https://rotana.net/en/streams?channel=439&itemId=736970') {
return Promise.resolve({ return Promise.resolve({
data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html'))
@ -52,11 +52,13 @@ it('can generate valid arabic url', () => {
}) })
it('can parse english response', async () => { it('can parse english response', async () => {
const result = (await parser({ const result = (
await parser({
channel, channel,
date, date,
content: fs.readFileSync(path.join(__dirname, '/__data__/content_en.html')) content: fs.readFileSync(path.join(__dirname, '/__data__/content_en.html'))
})).map(a => { })
).map(a => {
a.start = a.start.toJSON() a.start = a.start.toJSON()
a.stop = a.stop.toJSON() a.stop = a.stop.toJSON()
return a return a
@ -69,17 +71,20 @@ it('can parse english response', async () => {
title: 'Khiyana Mashroua', title: 'Khiyana Mashroua',
description: description:
'Hisham knows that his father has given all his wealth to his elder brother. This leads him to plan to kill his brother to make it look like a defense of honor, which he does by killing his wife along...', 'Hisham knows that his father has given all his wealth to his elder brother. This leads him to plan to kill his brother to make it look like a defense of honor, which he does by killing his wife along...',
image: 'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565', image:
'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565',
category: 'Movie' category: 'Movie'
}) })
}) })
it('can parse arabic response', async () => { it('can parse arabic response', async () => {
const result = (await parser({ const result = (
await parser({
channel: channelAr, channel: channelAr,
date, date,
content: fs.readFileSync(path.join(__dirname, '/__data__/content_ar.html')) content: fs.readFileSync(path.join(__dirname, '/__data__/content_ar.html'))
})).map(a => { })
).map(a => {
a.start = a.start.toJSON() a.start = a.start.toJSON()
a.stop = a.stop.toJSON() a.stop = a.stop.toJSON()
return a return a
@ -92,7 +97,8 @@ it('can parse arabic response', async () => {
title: 'خيانة مشروعة', title: 'خيانة مشروعة',
description: description:
'يعلم هشام البحيري أن والده قد حرمه من الميراث، ووهب كل ثروته لشقيقه اﻷكبر، وهو ما يدفعه لتدبير جريمة قتل شقيقه لتبدو وكأنها دفاع عن الشرف، وذلك حين يقتل هشام زوجته مع شقيقه.', 'يعلم هشام البحيري أن والده قد حرمه من الميراث، ووهب كل ثروته لشقيقه اﻷكبر، وهو ما يدفعه لتدبير جريمة قتل شقيقه لتبدو وكأنها دفاع عن الشرف، وذلك حين يقتل هشام زوجته مع شقيقه.',
image: 'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565', image:
'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565',
category: 'فيلم' category: 'فيلم'
}) })
}) })

View file

@ -53,7 +53,7 @@ async function parseItems(buffer) {
let data let data
try { try {
data = await pdf(buffer) data = await pdf(buffer)
} catch (err) { } catch {
return [] return []
} }

View file

@ -1,5 +1,4 @@
const _ = require('lodash') const _ = require('lodash')
const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')

View file

@ -1,5 +1,5 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const duration = require("dayjs/plugin/duration") const duration = require('dayjs/plugin/duration')
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')
@ -50,13 +50,13 @@ module.exports = {
} }
} }
function parseImage(item) { function parseImage() {
// Should return a string if we can output an image URL // Should return a string if we can output an image URL
// Might be done with `https://s.mxtv.jp/bangumi/link/weblinkU.csv?1722421896752` ? // Might be done with `https://s.mxtv.jp/bangumi/link/weblinkU.csv?1722421896752` ?
return null return null
} }
function parseCategory(item) { function parseCategory() {
// Should return a string if we can determine the category // Should return a string if we can determine the category
// Might be done with `https://s.mxtv.jp/index_set/csv/ranking_bangumi_allU.csv` ? // Might be done with `https://s.mxtv.jp/index_set/csv/ranking_bangumi_allU.csv` ?
return null return null
@ -68,12 +68,14 @@ function parseStart(item) {
function parseStop(item) { function parseStop(item) {
// Add the duration to the start time // Add the duration to the start time
const durationDate = dayjs(item.Duration, 'HH:mm:ss'); const durationDate = dayjs(item.Duration, 'HH:mm:ss')
return parseStart(item).add(dayjs.duration({ return parseStart(item).add(
dayjs.duration({
hours: durationDate.hour(), hours: durationDate.hour(),
minutes: durationDate.minute(), minutes: durationDate.minute(),
seconds: durationDate.second() seconds: durationDate.second()
})) })
)
} }
function parseItems(content) { function parseItems(content) {

View file

@ -11,7 +11,8 @@ const channel = {
name: 'Tokyo MX2', name: 'Tokyo MX2',
xmltv_id: 'TokyoMX2.jp' xmltv_id: 'TokyoMX2.jp'
} }
const content = `[{ "Event_id": "0x6a57", "Start_time": "2024年07月27日05時00分00秒", "Duration": "01:00:00", "Event_name": "ヒーリングタイム&ヘッドラインニュース", "Event_text": "ねこの足跡", "Component": "480i 16:9 パンベクトルなし", "Sound": "ステレオ", "Event_detail": ""}]` const content =
'[{ "Event_id": "0x6a57", "Start_time": "2024年07月27日05時00分00秒", "Duration": "01:00:00", "Event_name": "ヒーリングタイム&ヘッドラインニュース", "Event_text": "ねこの足跡", "Component": "480i 16:9 パンベクトルなし", "Sound": "ステレオ", "Event_detail": ""}]'
it('can generate valid url', () => { it('can generate valid url', () => {
const result = url({ date, channel }) const result = url({ date, channel })

View file

@ -114,7 +114,7 @@ module.exports = {
const $ = cheerio.load(data) const $ = cheerio.load(data)
$('.main-container-channels-events > .container-channel-events').each((i, el) => { $('.main-container-channels-events > .container-channel-events').each((i, el) => {
const name = $(el).find('.channel-title').text().trim() const name = $(el).find('.channel-title').text().trim()
const channelId = name.replace(/\s\&\s/gi, ' &amp; ') const channelId = name.replace(/\s&\s/gi, ' &amp; ')
if (!name) return if (!name) return

View file

@ -10,12 +10,15 @@ dayjs.extend(customParseFormat)
module.exports = { module.exports = {
site: 'shahid.mbc.net', site: 'shahid.mbc.net',
days: 2, days: 2,
url({ channel, date}) { url({ channel, date }) {
return `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${channel.site_id}&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format('YYYY-MM-DD')}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}` return `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${
channel.site_id
}&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format(
'YYYY-MM-DD'
)}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}`
}, },
parser({ content, channel }) { parser({ content, channel }) {
const programs = parseItems(content, channel) const programs = parseItems(content, channel).map(item => {
.map(item => {
return { return {
title: item.title, title: item.title,
description: item.description, description: item.description,
@ -28,13 +31,15 @@ module.exports = {
return programs return programs
}, },
async channels({lang = 'en'}) { async channels({ lang = 'en' }) {
const axios = require('axios') const axios = require('axios')
const items = [] const items = []
let page = 0 let page = 0
while (true) { while (true) {
const result = await axios const result = await axios
.get(`https://api2.shahid.net/proxy/v2.1/product/filter?filter=%7B"pageNumber":${page},"pageSize":100,"productType":"LIVESTREAM","productSubType":"LIVE_CHANNEL"%7D&country=SA&language=${lang}&Accept-Language=${lang}`) .get(
`https://api2.shahid.net/proxy/v2.1/product/filter?filter=%7B"pageNumber":${page},"pageSize":100,"productType":"LIVESTREAM","productSubType":"LIVE_CHANNEL"%7D&country=SA&language=${lang}&Accept-Language=${lang}`
)
.then(response => response.data) .then(response => response.data)
.catch(console.error) .catch(console.error)
if (result.productList) { if (result.productList) {
@ -44,7 +49,7 @@ module.exports = {
continue continue
} }
} }
break; break
} }
const channels = items.map(channel => { const channels = items.map(channel => {
return { return {

View file

@ -9,7 +9,11 @@ const channel = { site_id: '996520', xmltv_id: 'AlAanTV.ae', lang: 'en' }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe(
`https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${channel.site_id}&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format('YYYY-MM-DD')}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}` `https://api2.shahid.net/proxy/v2.1/shahid-epg-api/?csvChannelIds=${
channel.site_id
}&from=${date.format('YYYY-MM-DD')}T00:00:00.000Z&to=${date.format(
'YYYY-MM-DD'
)}T23:59:59.999Z&country=SA&language=${channel.lang}&Accept-Language=${channel.lang}`
) )
}) })
@ -22,9 +26,9 @@ it('can parse response', () => {
{ {
start: '2023-11-10T21:00:00.000Z', start: '2023-11-10T21:00:00.000Z',
stop: '2023-11-10T21:30:00.000Z', stop: '2023-11-10T21:30:00.000Z',
title: 'Menassaatona Fi Osboo\'', title: "Menassaatona Fi Osboo'",
description: description:
'The presenter reviews the most prominent episodes of news programs produced by the channel\'s team on a weekly basis, which include the most important global updates and developments at all levels.' "The presenter reviews the most prominent episodes of news programs produced by the channel's team on a weekly basis, which include the most important global updates and developments at all levels."
} }
]) ])
}) })

View file

@ -40,7 +40,7 @@ module.exports = {
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://www.singtel.com/personal/products-services/tv/tv-programme-guide`) .get('https://www.singtel.com/personal/products-services/tv/tv-programme-guide')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)
@ -62,7 +62,7 @@ function parseItems(content, channel) {
try { try {
const data = JSON.parse(content) const data = JSON.parse(content)
return data && data[channel.site_id] ? data[channel.site_id] : [] return data && data[channel.site_id] ? data[channel.site_id] : []
} catch (err) { } catch {
return [] return []
} }
} }

View file

@ -44,7 +44,7 @@ module.exports = {
const cheerio = require('cheerio') const cheerio = require('cheerio')
const data = await axios const data = await axios
.get(`https://sjonvarp.is/`) .get('https://sjonvarp.is/')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -12,9 +12,7 @@ module.exports = {
site: 'sky.com', site: 'sky.com',
days: 2, days: 2,
url({ date, channel }) { url({ date, channel }) {
return `https://awk.epgsky.com/hawk/linear/schedule/${ return `https://awk.epgsky.com/hawk/linear/schedule/${date.format('YYYYMMDD')}/${
date.format('YYYYMMDD')
}/${
channel.site_id channel.site_id
}` }`
}, },
@ -27,8 +25,7 @@ module.exports = {
.filter(schedule => schedule.sid === channel.site_id) .filter(schedule => schedule.sid === channel.site_id)
.forEach(schedule => { .forEach(schedule => {
if (Array.isArray(schedule.events)) { if (Array.isArray(schedule.events)) {
schedule.events schedule.events.forEach(event => {
.forEach(event => {
const start = dayjs.utc(event.st * 1000) const start = dayjs.utc(event.st * 1000)
const stop = start.add(event.d, 's') const stop = start.add(event.d, 's')
programs.push({ programs.push({
@ -55,9 +52,11 @@ module.exports = {
if (queue.t === 'r') { if (queue.t === 'r') {
const $ = cheerio.load(res) const $ = cheerio.load(res)
const initialData = JSON.parse(decodeURIComponent($('#initialData').text())) const initialData = JSON.parse(decodeURIComponent($('#initialData').text()))
initialData.state.epgData.regions initialData.state.epgData.regions.forEach(region => {
.forEach(region => { queues.push({
queues.push({ t: 'c', url: `https://awk.epgsky.com/hawk/linear/services/${region.bouquet}/${region.subBouquet}` }) t: 'c',
url: `https://awk.epgsky.com/hawk/linear/services/${region.bouquet}/${region.subBouquet}`
})
}) })
} }
// process channels // process channels

View file

@ -15,9 +15,7 @@ const channel = {
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe( expect(url({ channel, date })).toBe('https://awk.epgsky.com/hawk/linear/schedule/20241214/4086')
'https://awk.epgsky.com/hawk/linear/schedule/20241214/4086'
)
}) })
it('can parse response', () => { it('can parse response', () => {
@ -34,7 +32,7 @@ it('can parse response', () => {
stop: '2024-12-13T23:00:00.000Z', stop: '2024-12-13T23:00:00.000Z',
title: 'The UnXplained With...', title: 'The UnXplained With...',
description: description:
'The Hunt for Jack the Ripper: Jack the Ripper\'s identity has eluded police, historians and armchair detectives for over a century. What do we know about the notorious killer? (S3, ep 21)', "The Hunt for Jack the Ripper: Jack the Ripper's identity has eluded police, historians and armchair detectives for over a century. What do we know about the notorious killer? (S3, ep 21)",
season: 4, season: 4,
episode: 14 episode: 14
}) })

View file

@ -1,4 +1,4 @@
const { parser, url, request } = require('./skylife.co.kr.config.js') const { parser, url } = require('./skylife.co.kr.config.js')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const dayjs = require('dayjs') const dayjs = require('dayjs')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -18,7 +18,9 @@ const exported = {
url: function ({ date, channel }) { url: function ({ date, channel }) {
let [type, ...code] = channel.site_id.split('_') let [type, ...code] = channel.site_id.split('_')
code = code.join('_') code = code.join('_')
return `https://www.skyperfectv.co.jp/program/schedule/${type}/channel:${code}/date:${date.format('YYMMDD')}` return `https://www.skyperfectv.co.jp/program/schedule/${type}/channel:${code}/date:${date.format(
'YYMMDD'
)}`
}, },
logo: function ({ channel }) { logo: function ({ channel }) {
return `https://www.skyperfectv.co.jp/library/common/img/channel/icon/basic/m_${channel.site_id.toLowerCase()}.gif` return `https://www.skyperfectv.co.jp/library/common/img/channel/icon/basic/m_${channel.site_id.toLowerCase()}.gif`
@ -28,7 +30,7 @@ const exported = {
const url = exported.url({ date, channel }) const url = exported.url({ date, channel })
const response = await axios.get(url, { const response = await axios.get(url, {
headers: { headers: {
'Cookie': 'adult_auth=true' Cookie: 'adult_auth=true'
} }
}) })
return response.data return response.data
@ -49,7 +51,7 @@ const exported = {
// the next `td` will be the next day // the next `td` will be the next day
const today = date.add(index + addition, 'd').tz('Asia/Tokyo') const today = date.add(index + addition, 'd').tz('Asia/Tokyo')
const parseTime = (timeString) => { const parseTime = timeString => {
// timeString is in the format "HH:mm" // timeString is in the format "HH:mm"
// replace `today` with the time from timeString // replace `today` with the time from timeString
const [hour, minute] = timeString.split(':').map(Number) const [hour, minute] = timeString.split(':').map(Number)
@ -59,7 +61,12 @@ const exported = {
const $element = $(element) // Wrap element with Cheerio const $element = $(element) // Wrap element with Cheerio
$element.find('.p-program__item').each((itemIndex, itemElement) => { $element.find('.p-program__item').each((itemIndex, itemElement) => {
const $itemElement = $(itemElement) // Wrap itemElement with Cheerio const $itemElement = $(itemElement) // Wrap itemElement with Cheerio
const [start, stop] = $itemElement.find('.p-program__range').first().text().split('〜').map(parseTime) const [start, stop] = $itemElement
.find('.p-program__range')
.first()
.text()
.split('〜')
.map(parseTime)
const title = $itemElement.find('.p-program__name').first().text() const title = $itemElement.find('.p-program__name').first().text()
const image = $itemElement.find('.js-program_thumbnail').first().attr('data-lazysrc') const image = $itemElement.find('.js-program_thumbnail').first().attr('data-lazysrc')
programs.push({ programs.push({
@ -91,10 +98,10 @@ const exported = {
return channels return channels
} }
const getChannels = async (type) => { const getChannels = async type => {
const response = await axios.get(`https://www.skyperfectv.co.jp/program/schedule/${type}/`, { const response = await axios.get(`https://www.skyperfectv.co.jp/program/schedule/${type}/`, {
headers: { headers: {
'Cookie': 'adult_auth=true;' Cookie: 'adult_auth=true;'
} }
}) })
return pageParser(response.data, type) return pageParser(response.data, type)

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,4 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const { DateTime } = require('luxon')
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')
@ -9,15 +8,6 @@ dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
const channel = [{ site_id: '1208', xmltv_id: 'AlAoula.ma', lang: 'ar' },
{ site_id: '4069', xmltv_id: 'Laayoune.ma', lang: 'ar' },
{ site_id: '4070', xmltv_id: 'Arryadia.ma', lang: 'ar' },
{ site_id: '4071', xmltv_id: 'Athaqafia.ma', lang: 'ar' },
{ site_id: '4072', xmltv_id: 'AlMaghribia.ma', lang: 'ar' },
{ site_id: '4073', xmltv_id: 'Assadissa.ma', lang: 'ar' },
{ site_id: '4075', xmltv_id: 'Tamazight.ma', lang: 'ar' }]
module.exports = { module.exports = {
site: 'snrt.ma', site: 'snrt.ma',
channels: 'snrt.ma.channels.xml', channels: 'snrt.ma.channels.xml',
@ -77,7 +67,6 @@ function parseStart($item, date) {
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'Africa/Casablanca') return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm:ss', 'Africa/Casablanca')
} }
function parseTitle($item) { function parseTitle($item) {
return $item('.program-title-sm').text().trim() return $item('.program-title-sm').text().trim()
} }

View file

@ -1,6 +1,4 @@
const { parser, url } = require('./snrt.ma.config.js') const { parser, url } = require('./snrt.ma.config.js')
const cheerio = require('cheerio')
const { DateTime } = require('luxon')
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')
@ -25,16 +23,14 @@ it('can parse response', () => {
}) })
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
"category": "القرآن الكريم", category: 'القرآن الكريم',
"description": "", description: '',
"start": "2024-12-19T06:00:00.000Z", start: '2024-12-19T06:00:00.000Z',
"stop": "2024-12-19T06:10:00.000Z", stop: '2024-12-19T06:30:00.000Z',
"stop": "2024-12-19T06:30:00.000Z", title: 'ﺍﻟﺴﻼﻡ ﺍﻟﻮﻃﻨﻲ + ﺍﻟﻘﺮﺁﻥ ﺍﻟﻜﺮﻳﻢ'
"title": "ﺍﻟﺴﻼﻡ ﺍﻟﻮﻃﻨﻲ + ﺍﻟﻘﺮﺁﻥ ﺍﻟﻜﺮﻳﻢ"
}) })
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ const result = parser({
date, date,

View file

@ -9,15 +9,9 @@ module.exports = {
url({ date, channel }) { url({ date, channel }) {
return `https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/schedules?locale=${ return `https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/schedules?locale=${
languages[channel.lang] languages[channel.lang]
}&locale_default=${ }&locale_default=${languages[channel.lang]}&device=1&in_channel_id=${
languages[channel.lang]
}&device=1&in_channel_id=${
channel.site_id channel.site_id
}&gt_end=${ }&gt_end=${date.unix()}&lt_start=${date.add(1, 'd').unix()}&limit=100&page=1`
date.unix()
}&lt_start=${
date.add(1, 'd').unix()
}&limit=100&page=1`
}, },
async parser({ content, date, channel }) { async parser({ content, date, channel }) {
const programs = [] const programs = []
@ -29,7 +23,11 @@ module.exports = {
} }
if (res.page && res.page.current < res.page.total) { if (res.page && res.page.current < res.page.total) {
res = await axios res = await axios
.get(module.exports.url({ date, channel }).replace(/page=(\d+)/, `page=${res.page.current + 1}`)) .get(
module.exports
.url({ date, channel })
.replace(/page=(\d+)/, `page=${res.page.current + 1}`)
)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
} else { } else {
@ -39,7 +37,7 @@ module.exports = {
} }
const season = s => { const season = s => {
if (s) { if (s) {
const [ , , n ] = s.match(/(S|Season )(\d+)/) || [null, null, null] const [, , n] = s.match(/(S|Season )(\d+)/) || [null, null, null]
if (n) { if (n) {
return parseInt(n) return parseInt(n)
} }
@ -66,11 +64,9 @@ module.exports = {
let page = 1 let page = 1
while (true) { while (true) {
const items = await axios const items = await axios
.get(`https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/channels?locale=${ .get(
languages[lang] `https://waf-starhub-metadata-api-p001.ifs.vubiquity.com/v3.1/epg/channels?locale=${languages[lang]}&locale_default=${languages[lang]}&device=1&limit=50&page=${page}`
}&locale_default=${ )
languages[lang]
}&device=1&limit=50&page=${page}`)
.then(r => r.data) .then(r => r.data)
.catch(console.error) .catch(console.error)
if (items.resources) { if (items.resources) {

View file

@ -1,4 +1,4 @@
const { parser, url, request } = require('./starhubtvplus.com.config.js') const { parser, url } = require('./starhubtvplus.com.config.js')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
@ -36,9 +36,12 @@ it('can parse response', async () => {
title: 'Northern Rexposure', title: 'Northern Rexposure',
subTitle: 'Hudson & Rex (Season 5)', subTitle: 'Hudson & Rex (Season 5)',
description: description:
'When Jesse\'s sister contacts him for help, he, Sarah and Rex head to Northern Ontario and find themselves in the middle of a deadly situation.', "When Jesse's sister contacts him for help, he, Sarah and Rex head to Northern Ontario and find themselves in the middle of a deadly situation.",
category: ['Drama'], category: ['Drama'],
image: ['https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=960&h=540', 'https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=341&h=192'], image: [
'https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=960&h=540',
'https://poster.starhubgo.com/poster/ch511_hudson_rex5.jpg?w=341&h=192'
],
season: 5, season: 5,
episode: 15, episode: 15,
rating: 'PG13' rating: 'PG13'

View file

@ -8,9 +8,7 @@ const debug = require('debug')('site:startimestv.com')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
doFetch doFetch.setDebugger(debug).setMaxWorker(5)
.setDebugger(debug)
.setMaxWorker(5)
module.exports = { module.exports = {
site: 'startimestv.com', site: 'startimestv.com',
@ -24,7 +22,8 @@ module.exports = {
const programs = [] const programs = []
if (content) { if (content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
$('.box .mask').toArray() $('.box .mask')
.toArray()
.forEach(el => { .forEach(el => {
let title = parseText($(el).find('h4')) let title = parseText($(el).find('h4'))
const [s, e] = title.substr(0, title.indexOf(' ')).split('-') || [null, null] const [s, e] = title.substr(0, title.indexOf(' ')).split('-') || [null, null]
@ -53,7 +52,8 @@ module.exports = {
// process area-id // process area-id
if (queue.t === 'a') { if (queue.t === 'a') {
const $ = cheerio.load(res) const $ = cheerio.load(res)
$('dd.update-areaID').toArray() $('dd.update-areaID')
.toArray()
.forEach(el => { .forEach(el => {
const dd = $(el) const dd = $(el)
const areaId = dd.attr('area-id') const areaId = dd.attr('area-id')
@ -72,7 +72,8 @@ module.exports = {
if (queue.t === 's') { if (queue.t === 's') {
if (res) { if (res) {
const $ = cheerio.load(res) const $ = cheerio.load(res)
$(`.channl .c`).toArray() $('.channl .c')
.toArray()
.forEach(el => { .forEach(el => {
// only process channel with schedule only // only process channel with schedule only
const clazz = $(el).attr('class') const clazz = $(el).attr('class')
@ -98,13 +99,10 @@ module.exports = {
} }
function parseText($item) { function parseText($item) {
let text = $item.text() let text = $item.text().replace(/\t/g, '').replace(/\n/g, ' ').trim()
.replace(/\t/g, '')
.replace(/\n/g, ' ')
.trim()
while (true) { while (true) {
if (text.match(/ /)) { if (text.match(/\s\s/)) {
text = text.replace(/ /g, ' ') text = text.replace(/\s\s/g, ' ')
continue continue
} }
break break

View file

@ -33,10 +33,10 @@ module.exports = {
return programs return programs
}, },
async channels({ country, lang }) { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://streamingtvguides.com/Preferences`) .get('https://streamingtvguides.com/Preferences')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -3,7 +3,7 @@ const dayjs = require('dayjs')
const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be' const API_STATIC_ENDPOINT = 'https://static.spark.telenet.tv/eng/web/epg-service-lite/be'
const API_PROD_ENDPOINT = 'https://spark-prod-be.gnp.cloud.telenet.tv/eng/web/linear-service/v2' const API_PROD_ENDPOINT = 'https://spark-prod-be.gnp.cloud.telenet.tv/eng/web/linear-service/v2'
const API_IMAGE_ENDPOINT = 'https://staticqbr-prod-be.gnp.cloud.telenet.tv/image-service'; const API_IMAGE_ENDPOINT = 'https://staticqbr-prod-be.gnp.cloud.telenet.tv/image-service'
module.exports = { module.exports = {
site: 'telenet.tv', site: 'telenet.tv',
@ -134,5 +134,5 @@ function parseEpisode(detail) {
} }
function parseIcon(item) { function parseIcon(item) {
return `${API_IMAGE_ENDPOINT}/intent/${item.id}/posterTile`; return `${API_IMAGE_ENDPOINT}/intent/${item.id}/posterTile`
} }

View file

@ -27,10 +27,10 @@ module.exports = {
return programs return programs
}, },
async channels({ lang }) { async channels() {
const axios = require('axios') const axios = require('axios')
const data = await axios const data = await axios
.get(`https://telkussa.fi/API/Channels`) .get('https://telkussa.fi/API/Channels')
.then(r => r.data) .then(r => r.data)
.catch(console.log) .catch(console.log)

View file

@ -11,8 +11,7 @@ dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
doFetch doFetch.setDebugger(debug)
.setDebugger(debug)
const tz = 'Asia/Jakarta' const tz = 'Asia/Jakarta'
@ -20,17 +19,14 @@ module.exports = {
site: 'tivie.id', site: 'tivie.id',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
return `https://tivie.id/channel/${ return `https://tivie.id/channel/${channel.site_id}/${date.format('YYYYMMDD')}`
channel.site_id
}/${
date.format('YYYYMMDD')
}`
}, },
async parser({ content, date }) { async parser({ content, date }) {
const programs = [] const programs = []
if (content) { if (content) {
const $ = cheerio.load(content) const $ = cheerio.load(content)
const items = $('ul[x-data] > li[id*="event-"] > div.w-full').toArray() const items = $('ul[x-data] > li[id*="event-"] > div.w-full')
.toArray()
.map(item => { .map(item => {
const $item = $(item) const $item = $(item)
const time = $item.find('div:nth-child(1) span:nth-child(1)') const time = $item.find('div:nth-child(1) span:nth-child(1)')
@ -47,7 +43,12 @@ module.exports = {
p.title = parseText(info) p.title = parseText(info)
} }
if (p.title) { if (p.title) {
const [, , season, episode] = p.title.match(/( S(\d+))?, Ep\. (\d+)/) || [null, null, null, null] const [, , season, episode] = p.title.match(/( S(\d+))?, Ep\. (\d+)/) || [
null,
null,
null,
null
]
if (season) { if (season) {
p.season = parseInt(season) p.season = parseInt(season)
} }
@ -63,7 +64,7 @@ module.exports = {
.map(i => { .map(i => {
const url = i.url const url = i.url
delete i.url delete i.url
return {i, url} return { i, url }
}) })
if (queues.length) { if (queues.length) {
await doFetch(queues, (queue, res) => { await doFetch(queues, (queue, res) => {
@ -84,7 +85,11 @@ module.exports = {
if (i < items.length - 1) { if (i < items.length - 1) {
items[i].stop = items[i + 1].start items[i].stop = items[i + 1].start
} else { } else {
items[i].stop = dayjs.tz(`${date.add(1, 'd').format('YYYY-MM-DD')} 00:00`, 'YYYY-MM-DD HH:mm', tz) items[i].stop = dayjs.tz(
`${date.add(1, 'd').format('YYYY-MM-DD')} 00:00`,
'YYYY-MM-DD HH:mm',
tz
)
} }
} }
// add programs // add programs
@ -116,13 +121,10 @@ module.exports = {
} }
function parseText($item) { function parseText($item) {
let text = $item.text() let text = $item.text().replace(/\t/g, '').replace(/\n/g, ' ').trim()
.replace(/\t/g, '')
.replace(/\n/g, ' ')
.trim()
while (true) { while (true) {
if (text.match(/ /)) { if (text.match(/\s\s/)) {
text = text.replace(/ /g, ' ') text = text.replace(/\s\s/g, ' ')
continue continue
} }
break break

View file

@ -20,10 +20,8 @@ const channel = {
axios.get.mockImplementation(url => { axios.get.mockImplementation(url => {
const urls = { const urls = {
'https://tivie.id/film/white-house-down-nwzDnwz9nAv6': 'https://tivie.id/film/white-house-down-nwzDnwz9nAv6': 'program01.html',
'program01.html', 'https://tivie.id/program/hudson-rex-s6-e14-nwzDnwvBmQr9': 'program02.html'
'https://tivie.id/program/hudson-rex-s6-e14-nwzDnwvBmQr9':
'program02.html',
} }
let data = '' let data = ''
if (urls[url] !== undefined) { if (urls[url] !== undefined) {
@ -38,9 +36,7 @@ it('can generate valid url', () => {
it('can parse response', async () => { it('can parse response', async () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html')) const content = fs.readFileSync(path.join(__dirname, '__data__', 'content.html'))
const results = ( const results = (await parser({ date, content, channel })).map(p => {
await parser({ date, content, channel })
).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
@ -53,7 +49,8 @@ it('can parse response', async () => {
title: 'White House Down', title: 'White House Down',
description: description:
'Saat melakukan tur di Gedung Putih bersama putrinya yang masih kecil, seorang perwira polisi beraksi untuk melindungi anaknya dan presiden dari sekelompok penjajah paramiliter bersenjata lengkap.', 'Saat melakukan tur di Gedung Putih bersama putrinya yang masih kecil, seorang perwira polisi beraksi untuk melindungi anaknya dan presiden dari sekelompok penjajah paramiliter bersenjata lengkap.',
image: 'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2023/09/65116c78791c2-1695640694.jpg?resize=480,270', image:
'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2023/09/65116c78791c2-1695640694.jpg?resize=480,270'
}) })
expect(results[2]).toMatchObject({ expect(results[2]).toMatchObject({
start: '2024-12-30T18:00:00.000Z', start: '2024-12-30T18:00:00.000Z',
@ -61,9 +58,10 @@ it('can parse response', async () => {
title: 'Hudson & Rex S6, Ep. 14', title: 'Hudson & Rex S6, Ep. 14',
description: description:
'Saat guru musik Jesse terbunuh di studio rekamannya, Charlie dan Rex menghubungkan kejahatan tersebut dengan pembunuhan yang tampaknya tak ada hubungannya.', 'Saat guru musik Jesse terbunuh di studio rekamannya, Charlie dan Rex menghubungkan kejahatan tersebut dengan pembunuhan yang tampaknya tak ada hubungannya.',
image: 'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2024/07/668b7ced47b25-1720417517.jpg?resize=480,270', image:
'https://i0.wp.com/is3.cloudhost.id/tivie/poster/2024/07/668b7ced47b25-1720417517.jpg?resize=480,270',
season: 6, season: 6,
episode: 14, episode: 14
}) })
}) })
@ -71,7 +69,7 @@ it('can handle empty guide', async () => {
const results = await parser({ const results = await parser({
date, date,
channel, channel,
content: '', content: ''
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View file

@ -73,7 +73,7 @@ function parseItems(content, channel) {
let parsed let parsed
try { try {
parsed = JSON.parse(content) parsed = JSON.parse(content)
} catch (error) { } catch {
return [] return []
} }
if (!parsed || !parsed.k) return [] if (!parsed || !parsed.k) return []

View file

@ -70,7 +70,7 @@ module.exports = {
async function getTotalPageCount(region) { async function getTotalPageCount(region) {
const data = await axios const data = await axios
.get(`https://tv.mail.ru/ajax/channel/list/`, { .get('https://tv.mail.ru/ajax/channel/list/', {
params: { page: 0 }, params: { page: 0 },
headers: { headers: {
cookie: `s=fver=0|geo=${region};` cookie: `s=fver=0|geo=${region};`

View file

@ -40,7 +40,7 @@ module.exports = {
let offset = 0 let offset = 0
while (offset !== undefined) { while (offset !== undefined) {
const data = await axios const data = await axios
.get(`https://web-api.tv.nu/tableauLinearChannels`, { .get('https://web-api.tv.nu/tableauLinearChannels', {
params: { params: {
modules, modules,
date: dayjs().format('YYYY-MM-DD'), date: dayjs().format('YYYY-MM-DD'),

View file

@ -24,7 +24,7 @@ module.exports = {
return programs return programs
}, },
async channels({ token, lang = en }) { async channels({ token, lang = 'en' }) {
const axios = require('axios') const axios = require('axios')
const ACCESS_TOKEN = token const ACCESS_TOKEN = token
? token ? token

View file

@ -2,9 +2,7 @@ const dayjs = require('dayjs')
const doFetch = require('@ntlab/sfetch') const doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:tv.yandex.ru') const debug = require('debug')('site:tv.yandex.ru')
doFetch doFetch.setDebugger(debug).setMaxWorker(10)
.setDebugger(debug)
.setMaxWorker(10)
// enable to fetch guide description but its take a longer time // enable to fetch guide description but its take a longer time
const detailedGuide = true const detailedGuide = true
@ -12,14 +10,16 @@ const detailedGuide = true
// update this data by heading to https://tv.yandex.ru and change the values accordingly // update this data by heading to https://tv.yandex.ru and change the values accordingly
const cookies = { const cookies = {
i: 'eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=', i: 'eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=',
spravka: 'dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1', spravka:
'dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1',
yandexuid: '1197179041732383499', yandexuid: '1197179041732383499',
yashr: '4682342911732383504', yashr: '4682342911732383504',
yuidss: '1197179041732383499', yuidss: '1197179041732383499',
user_display: 824, user_display: 824
} }
const headers = { const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0', 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0'
} }
const caches = {} const caches = {}
@ -50,7 +50,9 @@ module.exports = {
} }
caches[cacheid].forEach(schedule => { caches[cacheid].forEach(schedule => {
schedule.events schedule.events
.filter(event => event.channelFamilyId == channel.site_id && date.isSame(event.start, 'day')) .filter(
event => event.channelFamilyId == channel.site_id && date.isSame(event.start, 'day')
)
.forEach(event => { .forEach(event => {
if (events.indexOf(event.id) < 0) { if (events.indexOf(event.id) < 0) {
events.push(event.id) events.push(event.id)
@ -171,7 +173,10 @@ function parseContent(content, date, checkOnly = false) {
content = content.toString() content = content.toString()
} }
// got captcha, its look like our cookies has expired // got captcha, its look like our cookies has expired
if (content?.type === 'captcha' || (typeof content === 'string' && content.match(/SmartCaptcha/))) { if (
content?.type === 'captcha' ||
(typeof content === 'string' && content.match(/SmartCaptcha/))
) {
throw new Error('Got captcha, please goto https://tv.yandex.ru and update cookies!') throw new Error('Got captcha, please goto https://tv.yandex.ru and update cookies!')
} }
if (typeof content === 'object') { if (typeof content === 'object') {
@ -210,7 +215,7 @@ function parseContent(content, date, checkOnly = false) {
headers['X-User-Session-Id'] = sessionId headers['X-User-Session-Id'] = sessionId
} }
if (checkOnly && region && tvSk.key && sessionId) { if (checkOnly && region && tvSk.key && sessionId) {
valid = true; valid = true
} }
} }
} }
@ -220,8 +225,7 @@ function parseContent(content, date, checkOnly = false) {
function parseCookies(headers) { function parseCookies(headers) {
if (Array.isArray(headers['set-cookie'])) { if (Array.isArray(headers['set-cookie'])) {
headers['set-cookie'] headers['set-cookie'].forEach(cookie => {
.forEach(cookie => {
const [key, value] = cookie.split('; ')[0].split('=') const [key, value] = cookie.split('; ')[0].split('=')
if (cookies[key] !== value) { if (cookies[key] !== value) {
cookies[key] = value cookies[key] = value
@ -232,13 +236,20 @@ function parseCookies(headers) {
} }
function getSchedules(schedules) { function getSchedules(schedules) {
return schedules.filter(schedule => schedule.events.length); return schedules.filter(schedule => schedule.events.length)
} }
function getHeaders(data = {}) { function getHeaders(data = {}) {
return Object.assign({}, headers, { return Object.assign(
Cookie: Object.keys(cookies).map(cookie => `${cookie}=${cookies[cookie]}`).join('; ') {},
}, data) headers,
{
Cookie: Object.keys(cookies)
.map(cookie => `${cookie}=${cookies[cookie]}`)
.join('; ')
},
data
)
} }
function getUrl(date, region = null, page = null, event = null) { function getUrl(date, region = null, page = null, event = null) {
@ -253,7 +264,9 @@ function getUrl(date, region = null, page = null, event = null) {
url += `${url.endsWith('/') ? '' : '/'}event?eventId=${event.id}&programCoId=` url += `${url.endsWith('/') ? '' : '/'}event?eventId=${event.id}&programCoId=`
} }
if (date) { if (date) {
url += `${url.indexOf('?') < 0 ? '?' : '&'}date=${date.format('YYYY-MM-DD')}${!page ? '&grid=all' : ''}&period=all-day` url += `${url.indexOf('?') < 0 ? '?' : '&'}date=${date.format('YYYY-MM-DD')}${
!page ? '&grid=all' : ''
}&period=all-day`
} }
if (page && page.id !== undefined && page.offset !== undefined) { if (page && page.id !== undefined && page.offset !== undefined) {
url += `${url.indexOf('?') < 0 ? '?' : '&'}offset=${page.offset}` url += `${url.indexOf('?') < 0 ? '?' : '&'}offset=${page.offset}`
@ -266,7 +279,7 @@ function getUrl(date, region = null, page = null, event = null) {
function getQueue(url, referer) { function getQueue(url, referer) {
const data = { const data = {
'Origin': 'https://tv.yandex.ru', Origin: 'https://tv.yandex.ru'
} }
if (referer) { if (referer) {
data['Referer'] = referer data['Referer'] = referer

View file

@ -16,7 +16,7 @@ const channel = {
site_id: '16', site_id: '16',
xmltv_id: 'ChannelOne.ru' xmltv_id: 'ChannelOne.ru'
} }
axios.get.mockImplementation((url, opts) => { axios.get.mockImplementation(url => {
if (url === 'https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') { if (url === 'https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day') {
return Promise.resolve({ return Promise.resolve({
headers: {}, headers: {},
@ -29,7 +29,10 @@ axios.get.mockImplementation((url, opts) => {
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule.json')))
}) })
} }
if (url === 'https://tv.yandex.ru/api/120809/main/chunk?page=0&date=2023-11-26&period=all-day&offset=0&limit=11') { if (
url ===
'https://tv.yandex.ru/api/120809/main/chunk?page=0&date=2023-11-26&period=all-day&offset=0&limit=11'
) {
return Promise.resolve({ return Promise.resolve({
headers: {}, headers: {},
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule0.json'))) data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/schedule0.json')))
@ -44,9 +47,7 @@ axios.get.mockImplementation((url, opts) => {
}) })
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ date })).toBe( expect(url({ date })).toBe('https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day')
'https://tv.yandex.ru/?date=2023-11-26&grid=all&period=all-day'
)
}) })
it('can generate valid request headers', () => { it('can generate valid request headers', () => {
@ -63,9 +64,7 @@ it('can generate valid request headers', () => {
it('can parse response', async () => { it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const result = ( const result = (await parser({ content, date, channel })).map(p => {
await parser({ content, date, channel })
).map(p => {
p.start = p.start.toJSON() p.start = p.start.toJSON()
p.stop = p.stop.toJSON() p.stop = p.stop.toJSON()
return p return p
@ -77,7 +76,8 @@ it('can parse response', async () => {
stop: '2023-11-26T02:10:00.000Z', stop: '2023-11-26T02:10:00.000Z',
title: 'ПОДКАСТ.ЛАБ. Мелодии моей жизни', title: 'ПОДКАСТ.ЛАБ. Мелодии моей жизни',
category: 'досуг', category: 'досуг',
description: 'Впереди вся ночь и есть о чем поговорить. Фильмы, музыка, любовь, звезды, еда, мода, анекдоты, спорт, деньги, настоящее, будущее - все это в творческом эксперименте.\nЛариса Гузеева читает любовные письма. Леонид Якубович рассказывает, кого не берут в пилоты. Арина Холина - какой секс способен довести до мужа или до развода. Валерий Сюткин на ходу сочиняет песню для Карины Кросс и Вали Карнавал. Дмитрий Дибров дарит новую жизнь любимой \"Антропологии\". Денис Казанский - все о футболе, хоккее и не только.\n\"ПОДКАСТЫ. ЛАБ\" - серия подкастов разной тематики, которые невозможно проспать. Интеллектуальные дискуссии после полуночи с самыми компетентными экспертами и актуальными спикерами.' description:
'Впереди вся ночь и есть о чем поговорить. Фильмы, музыка, любовь, звезды, еда, мода, анекдоты, спорт, деньги, настоящее, будущее - все это в творческом эксперименте.\nЛариса Гузеева читает любовные письма. Леонид Якубович рассказывает, кого не берут в пилоты. Арина Холина - какой секс способен довести до мужа или до развода. Валерий Сюткин на ходу сочиняет песню для Карины Кросс и Вали Карнавал. Дмитрий Дибров дарит новую жизнь любимой "Антропологии". Денис Казанский - все о футболе, хоккее и не только.\n"ПОДКАСТЫ. ЛАБ" - серия подкастов разной тематики, которые невозможно проспать. Интеллектуальные дискуссии после полуночи с самыми компетентными экспертами и актуальными спикерами.'
} }
]) ])
}) })

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