mirror of
https://github.com/iptv-org/epg
synced 2026-05-09 19:07:03 -04:00
Replace LF endings with CRLF
This commit is contained in:
@@ -1,189 +1,189 @@
|
||||
const axios = require('axios')
|
||||
const cheerio = require('cheerio')
|
||||
const dayjs = require('dayjs')
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||
const doFetch = require('@ntlab/sfetch')
|
||||
const debug = require('debug')('site:rotana.net')
|
||||
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
doFetch.setCheckResult(false).setDebugger(debug)
|
||||
|
||||
const tz = 'Asia/Riyadh'
|
||||
const defaultHeaders = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0'
|
||||
}
|
||||
const cookies = {}
|
||||
|
||||
module.exports = {
|
||||
site: 'rotana.net',
|
||||
days: 2,
|
||||
url({ channel }) {
|
||||
return `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&tz=`
|
||||
},
|
||||
request: {
|
||||
headers: defaultHeaders,
|
||||
timeout: 15000
|
||||
},
|
||||
async parser({ content, headers, channel, date }) {
|
||||
const programs = []
|
||||
if (!cookies[channel.lang]) {
|
||||
cookies[channel.lang] = parseCookies(headers)
|
||||
}
|
||||
|
||||
const items = parseItems(content, date)
|
||||
if (items.length) {
|
||||
const queues = []
|
||||
for (const item of items) {
|
||||
const url = `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&itemId=${item.program}`
|
||||
const params = {
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
cookie: cookies[channel.lang]
|
||||
}
|
||||
}
|
||||
queues.push({ i: item, url, params })
|
||||
}
|
||||
await doFetch(queues, (queue, res) => {
|
||||
programs.push(parseProgram(queue.i, res))
|
||||
})
|
||||
}
|
||||
|
||||
return programs
|
||||
},
|
||||
async channels({ lang = 'en' }) {
|
||||
const result = await axios
|
||||
.get('https://rotana.net/api/channels')
|
||||
.then(response => response.data)
|
||||
.catch(console.error)
|
||||
|
||||
return result.data.map(item => {
|
||||
return {
|
||||
lang,
|
||||
site_id: item.id,
|
||||
name: item.name[lang]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseProgram(item, result) {
|
||||
const $ = cheerio.load(result)
|
||||
const details = $('.trending-info .row div > span')
|
||||
if (details.length) {
|
||||
for (const el of details[0].children) {
|
||||
switch (el.constructor.name) {
|
||||
case 'Text':
|
||||
if (item.description === undefined) {
|
||||
const desc = $(el).text().trim()
|
||||
if (desc) {
|
||||
item.description = desc
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'Element':
|
||||
if (el.name === 'span') {
|
||||
const [k, v] = $(el)
|
||||
.text()
|
||||
.split(':')
|
||||
.map(a => a.trim())
|
||||
switch (k) {
|
||||
case 'Category':
|
||||
case 'التصنيف':
|
||||
item.category = v
|
||||
break
|
||||
case 'Country':
|
||||
case 'البلد':
|
||||
item.country = v
|
||||
break
|
||||
case 'Director':
|
||||
case 'المخرج':
|
||||
item.director = v
|
||||
break
|
||||
case 'Language':
|
||||
case 'اللغة':
|
||||
item.language = v
|
||||
break
|
||||
case 'Release Year':
|
||||
case 'سنة الإصدار':
|
||||
item.date = v
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const img = $('.row > div > img')
|
||||
if (img.length) {
|
||||
item.image = img.attr('src')
|
||||
}
|
||||
delete item.program
|
||||
return item
|
||||
}
|
||||
|
||||
function parseItems(content, date) {
|
||||
const $ = cheerio.load(content)
|
||||
|
||||
const items = []
|
||||
let curDate
|
||||
$('.hour > div').each((_, item) => {
|
||||
const $item = $(item)
|
||||
if ($item.hasClass('bg')) {
|
||||
curDate = $item.attr('id')
|
||||
curDate = curDate.substr(curDate.indexOf('-') + 1).split('-')
|
||||
} else if ($item.hasClass('iq-accordion')) {
|
||||
const top = $item.find('.iq-accordion-block')
|
||||
const heading = top.find('.iq-accordion-title .big-title')
|
||||
if (heading.length) {
|
||||
const progId = top.attr('id')
|
||||
const title = heading
|
||||
.find('span:eq(1)')
|
||||
.text()
|
||||
.split('\n')
|
||||
.map(a => a.trim())
|
||||
.join(' ')
|
||||
const time = heading.find('span:eq(0)').text()
|
||||
const [d, m, y] = curDate
|
||||
items.push({
|
||||
program: progId.substr(progId.indexOf('-') + 1),
|
||||
title: title ? title.trim() : title,
|
||||
start: `${y}-${m}-${d} ${time.trim()}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
items.sort((a, b) => a.start.localeCompare(b.start))
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (i < items.length - 2) {
|
||||
items[i].stop = items[i + 1].start
|
||||
} else {
|
||||
const dt = dayjs.tz(items[i].start).add(1, 'd')
|
||||
items[i].stop = `${dt.format('YYYY-MM-DD')} 00:00`
|
||||
}
|
||||
}
|
||||
const expectedDate = `${date.format('YYYY-MM-DD')}`
|
||||
return items
|
||||
.filter(a => a.start.startsWith(expectedDate) || a.stop.startsWith(expectedDate))
|
||||
.map(a => {
|
||||
a.start = dayjs.tz(a.start, tz)
|
||||
a.stop = dayjs.tz(a.stop, tz)
|
||||
return a
|
||||
})
|
||||
}
|
||||
|
||||
function parseCookies(headers) {
|
||||
const cookies = []
|
||||
if (headers && Array.isArray(headers['set-cookie'])) {
|
||||
headers['set-cookie'].forEach(cookie => {
|
||||
cookies.push(cookie.split('; ')[0])
|
||||
})
|
||||
}
|
||||
return cookies.length ? cookies.join('; ') : null
|
||||
}
|
||||
const axios = require('axios')
|
||||
const cheerio = require('cheerio')
|
||||
const dayjs = require('dayjs')
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||
const doFetch = require('@ntlab/sfetch')
|
||||
const debug = require('debug')('site:rotana.net')
|
||||
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
doFetch.setCheckResult(false).setDebugger(debug)
|
||||
|
||||
const tz = 'Asia/Riyadh'
|
||||
const defaultHeaders = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0'
|
||||
}
|
||||
const cookies = {}
|
||||
|
||||
module.exports = {
|
||||
site: 'rotana.net',
|
||||
days: 2,
|
||||
url({ channel }) {
|
||||
return `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&tz=`
|
||||
},
|
||||
request: {
|
||||
headers: defaultHeaders,
|
||||
timeout: 15000
|
||||
},
|
||||
async parser({ content, headers, channel, date }) {
|
||||
const programs = []
|
||||
if (!cookies[channel.lang]) {
|
||||
cookies[channel.lang] = parseCookies(headers)
|
||||
}
|
||||
|
||||
const items = parseItems(content, date)
|
||||
if (items.length) {
|
||||
const queues = []
|
||||
for (const item of items) {
|
||||
const url = `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&itemId=${item.program}`
|
||||
const params = {
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
cookie: cookies[channel.lang]
|
||||
}
|
||||
}
|
||||
queues.push({ i: item, url, params })
|
||||
}
|
||||
await doFetch(queues, (queue, res) => {
|
||||
programs.push(parseProgram(queue.i, res))
|
||||
})
|
||||
}
|
||||
|
||||
return programs
|
||||
},
|
||||
async channels({ lang = 'en' }) {
|
||||
const result = await axios
|
||||
.get('https://rotana.net/api/channels')
|
||||
.then(response => response.data)
|
||||
.catch(console.error)
|
||||
|
||||
return result.data.map(item => {
|
||||
return {
|
||||
lang,
|
||||
site_id: item.id,
|
||||
name: item.name[lang]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseProgram(item, result) {
|
||||
const $ = cheerio.load(result)
|
||||
const details = $('.trending-info .row div > span')
|
||||
if (details.length) {
|
||||
for (const el of details[0].children) {
|
||||
switch (el.constructor.name) {
|
||||
case 'Text':
|
||||
if (item.description === undefined) {
|
||||
const desc = $(el).text().trim()
|
||||
if (desc) {
|
||||
item.description = desc
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'Element':
|
||||
if (el.name === 'span') {
|
||||
const [k, v] = $(el)
|
||||
.text()
|
||||
.split(':')
|
||||
.map(a => a.trim())
|
||||
switch (k) {
|
||||
case 'Category':
|
||||
case 'التصنيف':
|
||||
item.category = v
|
||||
break
|
||||
case 'Country':
|
||||
case 'البلد':
|
||||
item.country = v
|
||||
break
|
||||
case 'Director':
|
||||
case 'المخرج':
|
||||
item.director = v
|
||||
break
|
||||
case 'Language':
|
||||
case 'اللغة':
|
||||
item.language = v
|
||||
break
|
||||
case 'Release Year':
|
||||
case 'سنة الإصدار':
|
||||
item.date = v
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const img = $('.row > div > img')
|
||||
if (img.length) {
|
||||
item.image = img.attr('src')
|
||||
}
|
||||
delete item.program
|
||||
return item
|
||||
}
|
||||
|
||||
function parseItems(content, date) {
|
||||
const $ = cheerio.load(content)
|
||||
|
||||
const items = []
|
||||
let curDate
|
||||
$('.hour > div').each((_, item) => {
|
||||
const $item = $(item)
|
||||
if ($item.hasClass('bg')) {
|
||||
curDate = $item.attr('id')
|
||||
curDate = curDate.substr(curDate.indexOf('-') + 1).split('-')
|
||||
} else if ($item.hasClass('iq-accordion')) {
|
||||
const top = $item.find('.iq-accordion-block')
|
||||
const heading = top.find('.iq-accordion-title .big-title')
|
||||
if (heading.length) {
|
||||
const progId = top.attr('id')
|
||||
const title = heading
|
||||
.find('span:eq(1)')
|
||||
.text()
|
||||
.split('\n')
|
||||
.map(a => a.trim())
|
||||
.join(' ')
|
||||
const time = heading.find('span:eq(0)').text()
|
||||
const [d, m, y] = curDate
|
||||
items.push({
|
||||
program: progId.substr(progId.indexOf('-') + 1),
|
||||
title: title ? title.trim() : title,
|
||||
start: `${y}-${m}-${d} ${time.trim()}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
items.sort((a, b) => a.start.localeCompare(b.start))
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (i < items.length - 2) {
|
||||
items[i].stop = items[i + 1].start
|
||||
} else {
|
||||
const dt = dayjs.tz(items[i].start).add(1, 'd')
|
||||
items[i].stop = `${dt.format('YYYY-MM-DD')} 00:00`
|
||||
}
|
||||
}
|
||||
const expectedDate = `${date.format('YYYY-MM-DD')}`
|
||||
return items
|
||||
.filter(a => a.start.startsWith(expectedDate) || a.stop.startsWith(expectedDate))
|
||||
.map(a => {
|
||||
a.start = dayjs.tz(a.start, tz)
|
||||
a.stop = dayjs.tz(a.stop, tz)
|
||||
return a
|
||||
})
|
||||
}
|
||||
|
||||
function parseCookies(headers) {
|
||||
const cookies = []
|
||||
if (headers && Array.isArray(headers['set-cookie'])) {
|
||||
headers['set-cookie'].forEach(cookie => {
|
||||
cookies.push(cookie.split('; ')[0])
|
||||
})
|
||||
}
|
||||
return cookies.length ? cookies.join('; ') : null
|
||||
}
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
const { parser, url, request } = require('./rotana.net.config.js')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const axios = require('axios')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(utc)
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
const date = dayjs.utc('2024-11-26').startOf('d')
|
||||
const channel = {
|
||||
lang: 'en',
|
||||
site_id: '439',
|
||||
xmltv_id: 'RotanaCinemaMasr.sa'
|
||||
}
|
||||
const channelAr = Object.assign({}, channel, { lang: 'ar' })
|
||||
|
||||
axios.get.mockImplementation(url => {
|
||||
if (url === 'https://rotana.net/en/streams?channel=439&itemId=736970') {
|
||||
return Promise.resolve({
|
||||
data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html'))
|
||||
})
|
||||
}
|
||||
if (url === 'https://rotana.net/ar/streams?channel=439&itemId=736970') {
|
||||
return Promise.resolve({
|
||||
data: fs.readFileSync(path.resolve(__dirname, '__data__/program_ar.html'))
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: '' })
|
||||
})
|
||||
|
||||
it('can use defined user agent', () => {
|
||||
const result = request.headers['User-Agent']
|
||||
expect(result).toBe(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0'
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate valid english url', () => {
|
||||
const result = url({ channel, date })
|
||||
expect(result).toBe('https://rotana.net/en/streams?channel=439&tz=')
|
||||
})
|
||||
|
||||
it('can generate valid arabic url', () => {
|
||||
const result = url({ channel: channelAr, date })
|
||||
expect(result).toBe('https://rotana.net/ar/streams?channel=439&tz=')
|
||||
})
|
||||
|
||||
it('can parse english response', async () => {
|
||||
const result = (
|
||||
await parser({
|
||||
channel,
|
||||
date,
|
||||
content: fs.readFileSync(path.join(__dirname, '/__data__/content_en.html'))
|
||||
})
|
||||
).map(a => {
|
||||
a.start = a.start.toJSON()
|
||||
a.stop = a.stop.toJSON()
|
||||
return a
|
||||
})
|
||||
|
||||
expect(result.length).toBe(12)
|
||||
expect(result[11]).toMatchObject({
|
||||
start: '2024-11-26T20:00:00.000Z',
|
||||
stop: '2024-11-26T22:00:00.000Z',
|
||||
title: 'Khiyana Mashroua',
|
||||
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...',
|
||||
image:
|
||||
'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565',
|
||||
category: 'Movie'
|
||||
})
|
||||
})
|
||||
|
||||
it('can parse arabic response', async () => {
|
||||
const result = (
|
||||
await parser({
|
||||
channel: channelAr,
|
||||
date,
|
||||
content: fs.readFileSync(path.join(__dirname, '/__data__/content_ar.html'))
|
||||
})
|
||||
).map(a => {
|
||||
a.start = a.start.toJSON()
|
||||
a.stop = a.stop.toJSON()
|
||||
return a
|
||||
})
|
||||
|
||||
expect(result.length).toBe(12)
|
||||
expect(result[11]).toMatchObject({
|
||||
start: '2024-11-26T20:00:00.000Z',
|
||||
stop: '2024-11-26T22:00:00.000Z',
|
||||
title: 'خيانة مشروعة',
|
||||
description:
|
||||
'يعلم هشام البحيري أن والده قد حرمه من الميراث، ووهب كل ثروته لشقيقه اﻷكبر، وهو ما يدفعه لتدبير جريمة قتل شقيقه لتبدو وكأنها دفاع عن الشرف، وذلك حين يقتل هشام زوجته مع شقيقه.',
|
||||
image:
|
||||
'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565',
|
||||
category: 'فيلم'
|
||||
})
|
||||
})
|
||||
|
||||
it('can handle empty guide', async () => {
|
||||
const result = await parser({
|
||||
content: '<!DOCTYPE html><html><head></head><body></body></html>',
|
||||
date,
|
||||
channel
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
const { parser, url, request } = require('./rotana.net.config.js')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const axios = require('axios')
|
||||
const dayjs = require('dayjs')
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
const customParseFormat = require('dayjs/plugin/customParseFormat')
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(utc)
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
const date = dayjs.utc('2024-11-26').startOf('d')
|
||||
const channel = {
|
||||
lang: 'en',
|
||||
site_id: '439',
|
||||
xmltv_id: 'RotanaCinemaMasr.sa'
|
||||
}
|
||||
const channelAr = Object.assign({}, channel, { lang: 'ar' })
|
||||
|
||||
axios.get.mockImplementation(url => {
|
||||
if (url === 'https://rotana.net/en/streams?channel=439&itemId=736970') {
|
||||
return Promise.resolve({
|
||||
data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html'))
|
||||
})
|
||||
}
|
||||
if (url === 'https://rotana.net/ar/streams?channel=439&itemId=736970') {
|
||||
return Promise.resolve({
|
||||
data: fs.readFileSync(path.resolve(__dirname, '__data__/program_ar.html'))
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: '' })
|
||||
})
|
||||
|
||||
it('can use defined user agent', () => {
|
||||
const result = request.headers['User-Agent']
|
||||
expect(result).toBe(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 OPR/104.0.0.0'
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate valid english url', () => {
|
||||
const result = url({ channel, date })
|
||||
expect(result).toBe('https://rotana.net/en/streams?channel=439&tz=')
|
||||
})
|
||||
|
||||
it('can generate valid arabic url', () => {
|
||||
const result = url({ channel: channelAr, date })
|
||||
expect(result).toBe('https://rotana.net/ar/streams?channel=439&tz=')
|
||||
})
|
||||
|
||||
it('can parse english response', async () => {
|
||||
const result = (
|
||||
await parser({
|
||||
channel,
|
||||
date,
|
||||
content: fs.readFileSync(path.join(__dirname, '/__data__/content_en.html'))
|
||||
})
|
||||
).map(a => {
|
||||
a.start = a.start.toJSON()
|
||||
a.stop = a.stop.toJSON()
|
||||
return a
|
||||
})
|
||||
|
||||
expect(result.length).toBe(12)
|
||||
expect(result[11]).toMatchObject({
|
||||
start: '2024-11-26T20:00:00.000Z',
|
||||
stop: '2024-11-26T22:00:00.000Z',
|
||||
title: 'Khiyana Mashroua',
|
||||
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...',
|
||||
image:
|
||||
'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565',
|
||||
category: 'Movie'
|
||||
})
|
||||
})
|
||||
|
||||
it('can parse arabic response', async () => {
|
||||
const result = (
|
||||
await parser({
|
||||
channel: channelAr,
|
||||
date,
|
||||
content: fs.readFileSync(path.join(__dirname, '/__data__/content_ar.html'))
|
||||
})
|
||||
).map(a => {
|
||||
a.start = a.start.toJSON()
|
||||
a.stop = a.stop.toJSON()
|
||||
return a
|
||||
})
|
||||
|
||||
expect(result.length).toBe(12)
|
||||
expect(result[11]).toMatchObject({
|
||||
start: '2024-11-26T20:00:00.000Z',
|
||||
stop: '2024-11-26T22:00:00.000Z',
|
||||
title: 'خيانة مشروعة',
|
||||
description:
|
||||
'يعلم هشام البحيري أن والده قد حرمه من الميراث، ووهب كل ثروته لشقيقه اﻷكبر، وهو ما يدفعه لتدبير جريمة قتل شقيقه لتبدو وكأنها دفاع عن الشرف، وذلك حين يقتل هشام زوجته مع شقيقه.',
|
||||
image:
|
||||
'https://s3.eu-central-1.amazonaws.com/rotana.website/spider_storage/1398X1000/1687084565',
|
||||
category: 'فيلم'
|
||||
})
|
||||
})
|
||||
|
||||
it('can handle empty guide', async () => {
|
||||
const result = await parser({
|
||||
content: '<!DOCTYPE html><html><head></head><body></body></html>',
|
||||
date,
|
||||
channel
|
||||
})
|
||||
expect(result).toMatchObject([])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user