diff --git a/sites/rotana.net/__data__/content_ar.html b/sites/rotana.net/__data__/content_ar.html index 4c72c90d..fb783989 100644 --- a/sites/rotana.net/__data__/content_ar.html +++ b/sites/rotana.net/__data__/content_ar.html @@ -1,203 +1,51 @@
-
+
+

- السبت - 11/11/2023 + الأحد + 2023-12-10

+
-
-
+ +
+
+
- 02:00 + + 00:36 - حريم كريم + + كتكوت
- + +
+
+ +
+
+ +
+ + 02:46 + + + عيار ناري +
+
+ + +
+
\ No newline at end of file diff --git a/sites/rotana.net/__data__/content_en.html b/sites/rotana.net/__data__/content_en.html index 893fd79c..9a9e9ecf 100644 --- a/sites/rotana.net/__data__/content_en.html +++ b/sites/rotana.net/__data__/content_en.html @@ -1,203 +1,51 @@
-
+
+

- Saturday - 11/11/2023 + Sunday + 2023-12-10

+
-
-
+ +
+
+
- 02:00 + + 00:36 - Harim Karim + + Katkout
- + +
+
+ +
+
+ +
+ + 02:46 + + + Gunshot +
+
+ + +
+
\ No newline at end of file diff --git a/sites/rotana.net/__data__/program_ar.html b/sites/rotana.net/__data__/program_ar.html new file mode 100644 index 00000000..2da8a525 --- /dev/null +++ b/sites/rotana.net/__data__/program_ar.html @@ -0,0 +1,240 @@ +
+
+ كتكوت +
+
+
+ +
+
+
+ + diff --git a/sites/rotana.net/__data__/program_en.html b/sites/rotana.net/__data__/program_en.html new file mode 100644 index 00000000..ba3f772c --- /dev/null +++ b/sites/rotana.net/__data__/program_en.html @@ -0,0 +1,240 @@ +
+
+ Katkout +
+
+
+ +
+
+
+ + diff --git a/sites/rotana.net/rotana.net.config.js b/sites/rotana.net/rotana.net.config.js index 22dd63a9..0528a966 100644 --- a/sites/rotana.net/rotana.net.config.js +++ b/sites/rotana.net/rotana.net.config.js @@ -1,8 +1,10 @@ +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 debug = require('debug')('site:rotana.net') dayjs.extend(timezone) dayjs.extend(utc) @@ -11,80 +13,100 @@ dayjs.extend(customParseFormat) const headers = { '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}` + return `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&tz=` }, request: { headers, timeout: 15000 }, - parser({ content, date }) { + async parser({ content, headers, channel, date }) { const programs = [] + if (!cookies[channel.lang]) { + cookies[channel.lang] = parseCookies(headers) + } const items = parseItems(content, date) - items.forEach(item => { - const info = item.find('.iq-accordion-block > .iq-accordion-title .big-title span') - if (info.length) { - const details = item.find('.trending-info div > span') - const [ time, title ] = info.text().split('\n') - const [ _, duration, description ] = details.text().split('\n') - if (duration) { - const start = dayjs.tz(`${date.format('YYYY-MM-DD')} ${time.trim()}`, 'YYYY-MM-DD HH:mm', 'Asia/Riyadh') - const stop = addDuration(start, duration.trim()) - - programs.push({ - title: title.trim(), - description: description.trim(), - start: start.toISOString(), - stop: stop.toISOString() - }) - } + for (const item of items) { + const program = await parseProgram(item, channel) + if (program) { + programs.push(program) } - }) + } return programs }, - async channels({ lang = 'en'}) { - const axios = require('axios') - const options = {headers} + async channels({ lang = 'en' }) { const result = await axios - .get(`https://rotana.net/${lang}/streams`, options) + .get(`https://rotana.net/api/channels`) .then(response => response.data) .catch(console.error) - const $ = cheerio.load(result) - const items = $('#channels-list a').toArray() - const channels = items.map(item => { - const $item = $(item) - const data = $item.attr('href').match(/channel=([A-Za-z0-9]+)/) - - return { - lang, - site_id: data[1], - name: $item.text().trim() - } - }) - - return channels - } + return result.data.map(item => { + return { + lang, + site_id: item.id, + name: item.name[lang] + } + }) + } } -function addDuration(date, duration) { - const matches = duration.matchAll(/(\d+)(h|m|s|ms)/g) - while (true) { - const m = matches.next() - if (!m.value) { - break +async function parseProgram(item, channel, options = {}) { + options = options || {} + const deep = options.deep !== undefined ? options.deep : true + const raw = options.raw !== undefined ? options.raw : false + const top = item.find('.iq-accordion-block') + const info = top.find('.iq-accordion-title .big-title span') + if (info.length) { + const [ time, title ] = info.text().split('\n') + const [ d, m, y ] = item._date.split('-') + const start = dayjs.tz(`${y}-${m}-${d} ${time.trim()}`, 'YYYY-MM-DD HH:mm', 'Asia/Riyadh') + let description, icon, stop + if (deep) { + const pid = top.attr('id').split('-')[1] + if (pid) { + const url = `https://rotana.net/${channel.lang}/streams?channel=${channel.site_id}&itemId=${pid}` + const params = { + headers: Object.assign({}, headers, { 'X-Requested-With': 'XMLHttpRequest' }), + Cookie: cookies[channel.lang] + } + debug(`fetching description ${url}`) + const result = await axios + .get(url, params) + .then(response => response.data) + .catch(console.error) + + const $ = cheerio.load(result) + const details = $('.trending-info div > span') + if (details.length) { + description = details.text().split('\n')[3].trim() + } + const img = $('.row > div > img') + if (img.length) { + icon = img.attr('src') + } + } + if (item._next) { + const next = await parseProgram(item._next, channel, { deep: false, raw: true }) + if (next.start) { + stop = next.start + } + } } - if (m.value[1] && m.value[2]) { - date = date.add(parseInt(m.value[1]), m.value[2]) + return { + title: title?.trim(), + description: description?.trim(), + icon: icon, + start: raw ? start : start?.toISOString(), + stop: raw ? stop : stop?.toISOString() } } - return date } function parseItems(content, date) { @@ -93,14 +115,35 @@ function parseItems(content, date) { const expectedId = `item-${date.format('DD-MM-YYYY')}` let lastId - $('.hour > div').toArray().forEach(item => { + $('.hour > div').each((_, item) => { const $item = $(item) if ($item.hasClass('bg')) { lastId = $item.attr('id') - } else if ($item.hasClass('iq-accordion') && lastId === expectedId) { - result.push($item) + } else if ($item.hasClass('iq-accordion')) { + $item._date = lastId.substr(lastId.indexOf('-') + 1) + // is date match? + if (lastId === expectedId) { + // set next item + if (result.length) { + result[result.length - 1]._next = $item; + } + result.push($item) + } else if (result.length && !result[result.length - 1]._next) { + // set next item + result[result.length - 1]._next = $item + } } }) return result } + +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 +} \ No newline at end of file diff --git a/sites/rotana.net/rotana.net.test.js b/sites/rotana.net/rotana.net.test.js index 7fedef29..cb64373c 100644 --- a/sites/rotana.net/rotana.net.test.js +++ b/sites/rotana.net/rotana.net.test.js @@ -1,4 +1,7 @@ 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') @@ -6,17 +9,30 @@ const customParseFormat = require('dayjs/plugin/customParseFormat') dayjs.extend(customParseFormat) dayjs.extend(utc) -const date = dayjs.utc('2023-11-11').startOf('d') +jest.mock('axios') + +const date = dayjs.utc('2023-12-10').startOf('d') const channel = { lang: 'en', site_id: '439', xmltv_id: 'RotanaCinemaMasr.sa' } -const channelAr = { - lang: 'ar', - site_id: '439', - xmltv_id: 'RotanaCinemaMasr.sa' -} +const channelAr = Object.assign({}, channel, { lang: 'ar' }) + +axios.get.mockImplementation((url, opts) => { + if (url === 'https://rotana.net/en/streams?channel=439&itemId=239849') { + return Promise.resolve({ + data: fs.readFileSync(path.resolve(__dirname, '__data__/program_en.html')) + }) + } + if (url === 'https://rotana.net/ar/streams?channel=439&itemId=239849') { + 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'] @@ -27,56 +43,48 @@ it('can use defined user agent', () => { it('can generate valid english url', () => { const result = url({ channel, date }) - expect(result).toBe('https://rotana.net/en/streams?channel=439') + 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') + expect(result).toBe('https://rotana.net/ar/streams?channel=439&tz=') }) -it('can parse english response', () => { - const fs = require('fs') - const path = require('path') - - const result = parser({ +it('can parse english response', async () => { + const result = await parser({ channel, date, content: fs.readFileSync(path.join(__dirname, '/__data__/content_en.html')) }) - expect(result).toMatchObject([ - { - start: '2023-11-10T23:00:00.000Z', - stop: '2023-11-11T01:00:00.000Z', - title: 'Harim Karim', - description: - 'Karim and Jihan separate after a year of marriage due to her discovering his betrayal in her home. Karim tries to get his wife back, but she refuses. Karim calls his old colleague Maha to help him. Ho...' - } - ]) + expect(result[0]).toMatchObject({ + start: '2023-12-09T21:36:00.000Z', + stop: '2023-12-09T23:46:00.000Z', + title: 'Katkout', + description: + 'In a comic framework, the events of the film revolve around (Katkoot) Al-Saedi, whose aunt, the eldest of the Al-Saedi family, tries to force him to kill himself in order to ransom his family. A time...', + icon: 'https://imgsrv.rotana.net/spider_storage/1398X1000/1690882129.webp?w=450&fit=max' + }) }) -it('can parse arabic response', () => { - const fs = require('fs') - const path = require('path') - - const result = parser({ - channelAr, +it('can parse arabic response', async () => { + const result = await parser({ + channel: channelAr, date, content: fs.readFileSync(path.join(__dirname, '/__data__/content_ar.html')) }) - expect(result).toMatchObject([ - { - start: '2023-11-10T23:00:00.000Z', - stop: '2023-11-11T01:00:00.000Z', - title: 'حريم كريم', - description: - 'كريم وجيهان ينفصلا بعد عام من الزواج بسبب اكتشافها لخيانته في منزلها، يحاول كريم استعادة زوجته، لكنها ترفض، فيتصل كريم بزميلته القديمة مها، لتساعده، لكن متاعب تحدث بين مها وزوجها، فتأتي لتعيش مع كريم،...' - } - ]) + expect(result[0]).toMatchObject({ + start: '2023-12-09T21:36:00.000Z', + stop: '2023-12-09T23:46:00.000Z', + title: 'كتكوت', + description: + 'في إطار كوميدي تدور أحداث الفيلم، حول (كتكوت) الصعيدي الذي تحاول عمته كبيرة العائلة الصعيدية إجباره على تقديم نفسه للقتل ليفدي عائلته، ولكنه يهرب وتخطفه جهة أمنية لاكتشاف شبه كبير بينه وبين (يوسف خوري...', + icon: 'https://imgsrv.rotana.net/spider_storage/1398X1000/1690882129.webp?w=450&fit=max' + }) }) -it('can handle empty guide', () => { - const result = parser({ +it('can handle empty guide', async () => { + const result = await parser({ content: '', date, channel diff --git a/sites/rotana.net/rotana.net_ar.channels.xml b/sites/rotana.net/rotana.net_ar.channels.xml index c3d360c2..6082c909 100644 --- a/sites/rotana.net/rotana.net_ar.channels.xml +++ b/sites/rotana.net/rotana.net_ar.channels.xml @@ -1,10 +1,15 @@ + روتانا أتش دي + روتانا سينما فرنسا الرساله قناة رسالة الدولية ضحك وبس إل بي سي + M+ HD رومانس + روتانا أفلام + روتانا أمريكا روتانا سينما مصر روتانا سينما السعودية روتانا كلاسيك @@ -12,5 +17,7 @@ روتانا كوميدي روتانا دراما روتانا خليجية + روتانا كيدز + روتانا موسيقي سيدة الشاشة diff --git a/sites/rotana.net/rotana.net_en.channels.xml b/sites/rotana.net/rotana.net_en.channels.xml index b28cfdcf..43b72ef2 100644 --- a/sites/rotana.net/rotana.net_en.channels.xml +++ b/sites/rotana.net/rotana.net_en.channels.xml @@ -1,10 +1,15 @@ + Rotana HD + Rotana Cinema France Al Resalah Al Resalah International Dahk wa Bass LBC + M+ HD Romance + Rotana Aflam + Rotana USA Rotana Cinema Masr Rotana Cinema KSA Rotana Classic @@ -12,5 +17,7 @@ Rotana Comedy Rotana Drama Rotana Khalijea HD + Rotana Kids + Rotana Music Sayedat Alshasha