From 7afd3fe3feacdbe9e7bf5e196bd9963a906f7fa3 Mon Sep 17 00:00:00 2001 From: freearhey <7253922+freearhey@users.noreply.github.com> Date: Mon, 28 Jul 2025 04:07:40 +0300 Subject: [PATCH] Fix linter issues --- scripts/functions/functions.ts | 142 ++--- sites/guidetnt.com/guidetnt.com.config.js | 671 +++++++++++----------- sites/guidetnt.com/guidetnt.com.test.js | 168 +++--- sites/tataplay.com/tataplay.com.config.js | 160 +++--- sites/tataplay.com/tataplay.com.test.js | 176 +++--- 5 files changed, 671 insertions(+), 646 deletions(-) diff --git a/scripts/functions/functions.ts b/scripts/functions/functions.ts index 346c5c94..f97b84f8 100644 --- a/scripts/functions/functions.ts +++ b/scripts/functions/functions.ts @@ -1,65 +1,77 @@ -/** - * Sorts an array by the result of running each element through an iteratee function. - * Creates a shallow copy of the array before sorting to avoid mutating the original. - * - * @param {Array} arr - The array to sort - * @param {Function} fn - The iteratee function to compute sort values - * @returns {Array} A new sorted array - * - * @example - * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}]; - * sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}] - */ -export const sortBy = (arr: T[], fn: (item: T) => number | string): T[] => [...arr].sort((a, b) => fn(a) > fn(b) ? 1 : -1) - -/** - * Sorts an array by multiple criteria with customizable sort orders. - * Supports ascending (default) and descending order for each criterion. - * - * @param {Array} arr - The array to sort - * @param {Array} fns - Array of iteratee functions to compute sort values - * @param {Array} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc' - * @returns {Array} A new sorted array - * - * @example - * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}]; - * orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']); - * // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}] - */ -export const orderBy = (arr: Array, fns: Array<(item: unknown) => string | number>, orders: Array = []): Array => [...arr].sort((a, b) => - fns.reduce((acc, fn, i) => - acc || ((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1), 0) -) - -/** - * Creates a duplicate-free version of an array using an iteratee function to generate - * the criterion by which uniqueness is computed. Only the first occurrence of each - * element is kept. - * - * @param {Array} arr - The array to inspect - * @param {Function} fn - The iteratee function to compute uniqueness criterion - * @returns {Array} A new duplicate-free array - * - * @example - * const users = [{id: 1, name: 'john'}, {id: 2, name: 'jane'}, {id: 1, name: 'john'}]; - * uniqBy(users, x => x.id); // [{id: 1, name: 'john'}, {id: 2, name: 'jane'}] - */ -export const uniqBy = (arr: T[], fn: (item: T) => unknown): T[] => arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index) - -/** - * Converts a string to start case (capitalizes the first letter of each word). - * Handles camelCase, snake_case, kebab-case, and regular spaces. - * - * @param {string} str - The string to convert - * @returns {string} The start case string - * - * @example - * startCase('hello_world'); // "Hello World" - * startCase('helloWorld'); // "Hello World" - * startCase('hello-world'); // "Hello World" - * startCase('hello world'); // "Hello World" - */ -export const startCase = (str: string): string => str - .replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase - .replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces - .replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word \ No newline at end of file +/** + * Sorts an array by the result of running each element through an iteratee function. + * Creates a shallow copy of the array before sorting to avoid mutating the original. + * + * @param {Array} arr - The array to sort + * @param {Function} fn - The iteratee function to compute sort values + * @returns {Array} A new sorted array + * + * @example + * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}]; + * sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}] + */ +export const sortBy = (arr: T[], fn: (item: T) => number | string): T[] => + [...arr].sort((a, b) => (fn(a) > fn(b) ? 1 : -1)) + +/** + * Sorts an array by multiple criteria with customizable sort orders. + * Supports ascending (default) and descending order for each criterion. + * + * @param {Array} arr - The array to sort + * @param {Array} fns - Array of iteratee functions to compute sort values + * @param {Array} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc' + * @returns {Array} A new sorted array + * + * @example + * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}]; + * orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']); + * // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}] + */ +export const orderBy = ( + arr: Array, + fns: Array<(item: unknown) => string | number>, + orders: Array = [] +): Array => + [...arr].sort((a, b) => + fns.reduce( + (acc, fn, i) => + acc || + ((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1), + 0 + ) + ) + +/** + * Creates a duplicate-free version of an array using an iteratee function to generate + * the criterion by which uniqueness is computed. Only the first occurrence of each + * element is kept. + * + * @param {Array} arr - The array to inspect + * @param {Function} fn - The iteratee function to compute uniqueness criterion + * @returns {Array} A new duplicate-free array + * + * @example + * const users = [{id: 1, name: 'john'}, {id: 2, name: 'jane'}, {id: 1, name: 'john'}]; + * uniqBy(users, x => x.id); // [{id: 1, name: 'john'}, {id: 2, name: 'jane'}] + */ +export const uniqBy = (arr: T[], fn: (item: T) => unknown): T[] => + arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index) + +/** + * Converts a string to start case (capitalizes the first letter of each word). + * Handles camelCase, snake_case, kebab-case, and regular spaces. + * + * @param {string} str - The string to convert + * @returns {string} The start case string + * + * @example + * startCase('hello_world'); // "Hello World" + * startCase('helloWorld'); // "Hello World" + * startCase('hello-world'); // "Hello World" + * startCase('hello world'); // "Hello World" + */ +export const startCase = (str: string): string => + str + .replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase + .replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces + .replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word diff --git a/sites/guidetnt.com/guidetnt.com.config.js b/sites/guidetnt.com/guidetnt.com.config.js index 9e00934c..092871f6 100755 --- a/sites/guidetnt.com/guidetnt.com.config.js +++ b/sites/guidetnt.com/guidetnt.com.config.js @@ -1,333 +1,338 @@ -const cheerio = require('cheerio') -const axios = require('axios') -const dayjs = require('dayjs') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -require('dayjs/locale/fr') - -dayjs.extend(customParseFormat) -dayjs.extend(utc) -dayjs.extend(timezone) - -const PARIS_TZ = 'Europe/Paris' - -module.exports = { - site: 'guidetnt.com', - days: 2, - url({ channel, date }) { - const now = dayjs() - const demain = now.add(1, 'd') - if (date && date.isSame(demain, 'day')) { - return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}` - } else if (!date || date.isSame(now, 'day')) { - return `https://www.guidetnt.com/tv/programme-${channel.site_id}` - } else { - return null - } - }, - async parser({ content, date }) { - const programs = [] - const allItems = parseItems(content) - const items = allItems?.rows - const itemDate = allItems?.formattedDate - for (const item of items) { - const prev = programs[programs.length - 1] - const $item = cheerio.load(item) - const title = parseTitle($item) - let start = parseStart($item, itemDate) - - if (!start || !title) return - if (prev) { - if (start.isBefore(prev.start)) { - start = start.add(1, 'd') - date = date.add(1, 'd') - } - prev.stop = start - } - let stop = start.add(30, 'm') - - let itemDetails = null - let subTitle = null - //let duration = null - let country = null - let productionDate = null - let episode = null - let season = null - let category = parseCategory($item) - let description = parseDescription($item) - const itemDetailsURL = parseDescriptionURL($item) - if(itemDetailsURL) { - const url = 'https://www.guidetnt.com' + itemDetailsURL - try { - const response = await axios.get(url) - itemDetails = parseItemDetails(response.data) - } catch (err) { - console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, err) - } - - const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD')) - start = timeRange?.start - stop = timeRange?.stop - - subTitle = itemDetails?.subTitle - if (title == subTitle) subTitle = null - description = itemDetails?.description - - const categoryDetails = parseCategoryText(itemDetails?.category) - //duration = categoryDetails?.duration - country = categoryDetails?.country - productionDate = categoryDetails?.productionDate - season = categoryDetails?.season - episode = categoryDetails?.episode - } - // See https://www.npmjs.com/package/epg-parser for parameters - programs.push({ - title, - subTitle: subTitle, - description: description, - image: itemDetails?.image, - category: category, - directors: itemDetails?.directorActors?.Réalisateur, - actors: itemDetails?.directorActors?.Acteur, - country: country, - date: productionDate, - //duration: duration, // Tried with length: too, but does not work ! (stop-start is not accurate because of Ads) - season: season, - episode: episode, - start, - stop - }) - } - - return programs - }, - async channels() { - const response = await axios.get('https://www.guidetnt.com') - const channels = [] - const $ = cheerio.load(response.data) - - // Look inside each .tvlogo container - $('.tvlogo').each((i, el) => { - // Find all descendants that have an alt attribute - $(el).find('[alt]').each((j, subEl) => { - const alt = $(subEl).attr('alt') - const href = $(subEl).attr('href') - if (href && alt && alt.trim() !== '') { - const name = alt.trim() - const site_id = href.replace(/^\/tv\/programme-/, '') - channels.push({ - lang: 'fr', - name, - site_id - }) - } - }) - }) - return channels - } -} - -function parseTimeRange(timeRange, baseDate) { - // Split times - const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim()) - - // Parse with base date - const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm') - let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm') - - // Handle possible day wrap (e.g., 23:30 - 00:15) - if (end.isBefore(start)) { - end = end.add(1, 'day') - } - - // Calculate duration in minutes - const diffMinutes = end.diff(start, 'minute') - - return { - start: start.format(), - stop: end.format(), - duration: diffMinutes - } -} - -function parseItemDetails(itemDetails) { - const $ = cheerio.load(itemDetails) - - const program = $('.program-wrapper').first() - - const programHour = program.find('.program-hour').text().trim() - const programTitle = program.find('.program-title').text().trim() - const programElementBold = program.find('.program-element-bold').text().trim() - const programArea1 = program.find('.program-element.program-area-1').text().trim() - - let description = '' - const programElements = $('.program-element').filter((i, el) => { - const classAttr = $(el).attr('class') - // Return true only if it is exactly "program-element" (no extra classes) - return classAttr.trim() === 'program-element' - }) - - programElements.each((i, el) => { - description += $(el).text().trim() - }) - - const area2Node = $('.program-area-2').first() - const area2 = $(area2Node) - const data = {} - let currentLabel = null - let texts = [] - - area2.contents().each((i, node) => { - if (node.type === 'tag' && node.name === 'strong') { - // If we had collected some text for the previous label, save it - if (currentLabel && texts.length) { - data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma - } - // New label - get text without colon - currentLabel = $(node).text().replace(/:$/, '').trim() - texts = [] - } else if (currentLabel) { - // Append the text content (text node or others) - if (node.type === 'text') { - texts.push(node.data) - } else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') { - texts.push($(node).text()) - } - } - }) - - // Save last label text - if (currentLabel && texts.length) { - data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') - } - - const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null - - return { - programHour, - title: programTitle, - subTitle: programElementBold, - category: programArea1, - description: description, - directorActors: data, - image: imgSrc - } -} - -function parseCategoryText(text) { - if (!text) return null - - const parts = text.split(',').map(s => s.trim()).filter(Boolean) - const len = parts.length - - const category = parts[0] || null - - if (len < 3) { - return { - category: category, - duration: null, - country: null, - productionDate: null, - season: null, - episode: null - } - } - - // Check last part: date if numeric - const dateCandidate = parts[len - 1] - const productionDate = /^\d{4}$/.test(dateCandidate) ? dateCandidate : null - - // Check for duration (first part containing "minutes") - let durationMinute = null - //let duration = null - let episode = null - let season = null - let durationIndex = -1 - for (let i = 0; i < len; i++) { - if (parts[i].toLowerCase().includes('minute')) { - durationMinute = parts[i].trim() - durationMinute = durationMinute.replace('minutes', '') - durationMinute = durationMinute.replace('minute', '') - //duration = [{ units: 'minutes', value: durationMinute }], - durationIndex = i - } else if (parts[i].toLowerCase().includes('épisode')) { - const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i) - if (match) { - episode = parseInt(match[1], 10) - } - } else if (parts[i].toLowerCase().includes('saison')) { - season = parts[i].replace('saison', '').trim() - } - } - - // Country: second to last - const countryIndex = len - 2 - let country = (durationIndex === countryIndex) ? null : parts[countryIndex] - - return { - category, - durationMinute, - country, - productionDate, - season, - episode - } -} - -function parseTitle($item) { - return $item('.channel-programs-title a').text().trim() -} - -function parseDescription($item) { - return $item('#descr').text().trim() || null -} - -function parseDescriptionURL($item) { - const descrLink = $item('#descr a') - return descrLink.attr('href') || null -} - -function parseCategory($item) { - let type = null - $item('.channel-programs-title span').each((i, span) => { - const className = $item(span).attr('class') - if (className && className.startsWith('text_bg')) { - type = $item(span).text().trim() - } - }) - return type -} - -function parseStart($item, itemDate) { - const dt = $item('.channel-programs-time a').text().trim() - if (!dt) return null - - const datetimeStr = `${itemDate} ${dt}` - return dayjs.tz(datetimeStr, 'YYYY-MM-DD HH:mm', PARIS_TZ) -} - -function parseItems(content) { - const $ = cheerio.load(content) - - // Extract header information - const logoSrc = $('#logo img').attr('src') - const title = $('#title h1').text().trim() - const subtitle = $('#subtitle').text().trim() - const dateMatch = subtitle.match(/(\d{1,2} \w+ \d{4})/) - const dateStr = dateMatch ? dateMatch[1].toLowerCase() : null - - // Parse the French date string - const parsedDate = dayjs(dateStr, 'D MMMM YYYY', 'fr') - // Format it as YYYY-MM-DD - const formattedDate = parsedDate.format('YYYY-MM-DD') - - const rows = $('.channel-row').toArray() - - return { - rows, - logoSrc, - title, - formattedDate - } -} \ No newline at end of file +const cheerio = require('cheerio') +const axios = require('axios') +const dayjs = require('dayjs') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +require('dayjs/locale/fr') + +dayjs.extend(customParseFormat) +dayjs.extend(utc) +dayjs.extend(timezone) + +const PARIS_TZ = 'Europe/Paris' + +module.exports = { + site: 'guidetnt.com', + days: 2, + url({ channel, date }) { + const now = dayjs() + const demain = now.add(1, 'd') + if (date && date.isSame(demain, 'day')) { + return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}` + } else if (!date || date.isSame(now, 'day')) { + return `https://www.guidetnt.com/tv/programme-${channel.site_id}` + } else { + return null + } + }, + async parser({ content, date }) { + const programs = [] + const allItems = parseItems(content) + const items = allItems?.rows + const itemDate = allItems?.formattedDate + for (const item of items) { + const prev = programs[programs.length - 1] + const $item = cheerio.load(item) + const title = parseTitle($item) + let start = parseStart($item, itemDate) + + if (!start || !title) return + if (prev) { + if (start.isBefore(prev.start)) { + start = start.add(1, 'd') + date = date.add(1, 'd') + } + prev.stop = start + } + let stop = start.add(30, 'm') + + let itemDetails = null + let subTitle = null + //let duration = null + let country = null + let productionDate = null + let episode = null + let season = null + let category = parseCategory($item) + let description = parseDescription($item) + const itemDetailsURL = parseDescriptionURL($item) + if (itemDetailsURL) { + const url = 'https://www.guidetnt.com' + itemDetailsURL + try { + const response = await axios.get(url) + itemDetails = parseItemDetails(response.data) + } catch (err) { + console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, err) + } + + const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD')) + start = timeRange?.start + stop = timeRange?.stop + + subTitle = itemDetails?.subTitle + if (title == subTitle) subTitle = null + description = itemDetails?.description + + const categoryDetails = parseCategoryText(itemDetails?.category) + //duration = categoryDetails?.duration + country = categoryDetails?.country + productionDate = categoryDetails?.productionDate + season = categoryDetails?.season + episode = categoryDetails?.episode + } + // See https://www.npmjs.com/package/epg-parser for parameters + programs.push({ + title, + subTitle: subTitle, + description: description, + image: itemDetails?.image, + category: category, + directors: itemDetails?.directorActors?.Réalisateur, + actors: itemDetails?.directorActors?.Acteur, + country: country, + date: productionDate, + //duration: duration, // Tried with length: too, but does not work ! (stop-start is not accurate because of Ads) + season: season, + episode: episode, + start, + stop + }) + } + + return programs + }, + async channels() { + const response = await axios.get('https://www.guidetnt.com') + const channels = [] + const $ = cheerio.load(response.data) + + // Look inside each .tvlogo container + $('.tvlogo').each((i, el) => { + // Find all descendants that have an alt attribute + $(el) + .find('[alt]') + .each((j, subEl) => { + const alt = $(subEl).attr('alt') + const href = $(subEl).attr('href') + if (href && alt && alt.trim() !== '') { + const name = alt.trim() + const site_id = href.replace(/^\/tv\/programme-/, '') + channels.push({ + lang: 'fr', + name, + site_id + }) + } + }) + }) + return channels + } +} + +function parseTimeRange(timeRange, baseDate) { + // Split times + const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim()) + + // Parse with base date + const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm') + let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm') + + // Handle possible day wrap (e.g., 23:30 - 00:15) + if (end.isBefore(start)) { + end = end.add(1, 'day') + } + + // Calculate duration in minutes + const diffMinutes = end.diff(start, 'minute') + + return { + start: start.format(), + stop: end.format(), + duration: diffMinutes + } +} + +function parseItemDetails(itemDetails) { + const $ = cheerio.load(itemDetails) + + const program = $('.program-wrapper').first() + + const programHour = program.find('.program-hour').text().trim() + const programTitle = program.find('.program-title').text().trim() + const programElementBold = program.find('.program-element-bold').text().trim() + const programArea1 = program.find('.program-element.program-area-1').text().trim() + + let description = '' + const programElements = $('.program-element').filter((i, el) => { + const classAttr = $(el).attr('class') + // Return true only if it is exactly "program-element" (no extra classes) + return classAttr.trim() === 'program-element' + }) + + programElements.each((i, el) => { + description += $(el).text().trim() + }) + + const area2Node = $('.program-area-2').first() + const area2 = $(area2Node) + const data = {} + let currentLabel = null + let texts = [] + + area2.contents().each((i, node) => { + if (node.type === 'tag' && node.name === 'strong') { + // If we had collected some text for the previous label, save it + if (currentLabel && texts.length) { + data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma + } + // New label - get text without colon + currentLabel = $(node).text().replace(/:$/, '').trim() + texts = [] + } else if (currentLabel) { + // Append the text content (text node or others) + if (node.type === 'text') { + texts.push(node.data) + } else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') { + texts.push($(node).text()) + } + } + }) + + // Save last label text + if (currentLabel && texts.length) { + data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') + } + + const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null + + return { + programHour, + title: programTitle, + subTitle: programElementBold, + category: programArea1, + description: description, + directorActors: data, + image: imgSrc + } +} + +function parseCategoryText(text) { + if (!text) return null + + const parts = text + .split(',') + .map(s => s.trim()) + .filter(Boolean) + const len = parts.length + + const category = parts[0] || null + + if (len < 3) { + return { + category: category, + duration: null, + country: null, + productionDate: null, + season: null, + episode: null + } + } + + // Check last part: date if numeric + const dateCandidate = parts[len - 1] + const productionDate = /^\d{4}$/.test(dateCandidate) ? dateCandidate : null + + // Check for duration (first part containing "minutes") + let durationMinute = null + //let duration = null + let episode = null + let season = null + let durationIndex = -1 + for (let i = 0; i < len; i++) { + if (parts[i].toLowerCase().includes('minute')) { + durationMinute = parts[i].trim() + durationMinute = durationMinute.replace('minutes', '') + durationMinute = durationMinute.replace('minute', '') + //duration = [{ units: 'minutes', value: durationMinute }], + durationIndex = i + } else if (parts[i].toLowerCase().includes('épisode')) { + const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i) + if (match) { + episode = parseInt(match[1], 10) + } + } else if (parts[i].toLowerCase().includes('saison')) { + season = parts[i].replace('saison', '').trim() + } + } + + // Country: second to last + const countryIndex = len - 2 + let country = durationIndex === countryIndex ? null : parts[countryIndex] + + return { + category, + durationMinute, + country, + productionDate, + season, + episode + } +} + +function parseTitle($item) { + return $item('.channel-programs-title a').text().trim() +} + +function parseDescription($item) { + return $item('#descr').text().trim() || null +} + +function parseDescriptionURL($item) { + const descrLink = $item('#descr a') + return descrLink.attr('href') || null +} + +function parseCategory($item) { + let type = null + $item('.channel-programs-title span').each((i, span) => { + const className = $item(span).attr('class') + if (className && className.startsWith('text_bg')) { + type = $item(span).text().trim() + } + }) + return type +} + +function parseStart($item, itemDate) { + const dt = $item('.channel-programs-time a').text().trim() + if (!dt) return null + + const datetimeStr = `${itemDate} ${dt}` + return dayjs.tz(datetimeStr, 'YYYY-MM-DD HH:mm', PARIS_TZ) +} + +function parseItems(content) { + const $ = cheerio.load(content) + + // Extract header information + const logoSrc = $('#logo img').attr('src') + const title = $('#title h1').text().trim() + const subtitle = $('#subtitle').text().trim() + const dateMatch = subtitle.match(/(\d{1,2} \w+ \d{4})/) + const dateStr = dateMatch ? dateMatch[1].toLowerCase() : null + + // Parse the French date string + const parsedDate = dayjs(dateStr, 'D MMMM YYYY', 'fr') + // Format it as YYYY-MM-DD + const formattedDate = parsedDate.format('YYYY-MM-DD') + + const rows = $('.channel-row').toArray() + + return { + rows, + logoSrc, + title, + formattedDate + } +} diff --git a/sites/guidetnt.com/guidetnt.com.test.js b/sites/guidetnt.com/guidetnt.com.test.js index 0ee3906a..299f19cf 100644 --- a/sites/guidetnt.com/guidetnt.com.test.js +++ b/sites/guidetnt.com/guidetnt.com.test.js @@ -1,83 +1,85 @@ -const { parser, url } = require('./guidetnt.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -const timezone = require('dayjs/plugin/timezone') -require('dayjs/locale/fr') -dayjs.extend(customParseFormat) -dayjs.extend(utc) -dayjs.extend(timezone) - -const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'tf1', - xmltv_id: 'TF1.fr' -} - -it('can generate valid url', () => { - expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1') -}) - -it('can parse response', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = await parser({ content, date }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(29) - expect(results[0]).toMatchObject({ - category: 'Série', - description: 'Grande effervescence pour toute l\'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d\'optimiser les révisions d\'E...', - start: '2025-06-30T22:55:00.000Z', - stop: '2025-06-30T23:45:00.000Z', - title: 'Camping Paradis' - }) - expect(results[2]).toMatchObject({ - category: 'Magazine', - description: 'Retrouvez tous vos programmes de nuit.', - start: '2025-07-01T00:55:00.000Z', - stop: '2025-07-01T04:00:00.000Z', - title: 'Programmes de la nuit' - }) - expect(results[15]).toMatchObject({ - category: 'Téléfilm', - description: 'La vie quasi parfaite de Riley bascule brutalement lorsqu\'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l\'enquête débute, l\'affaire prend une tournure étrange l...', - start: '2025-07-01T12:25:00.000Z', - stop: '2025-07-01T14:00:00.000Z', - title: 'Trahie par l\'amour' - }) -}) - -it('can parse response for current day', async () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) - let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') }) - results = results.map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - } - ) - - expect(results.length).toBe(29) - expect(results[0]).toMatchObject({ - category: 'Série', - description: 'Grande effervescence pour toute l\'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d\'optimiser les révisions d\'E...', - start: '2025-06-30T22:55:00.000Z', - stop: '2025-06-30T23:45:00.000Z', - title: 'Camping Paradis' - }) -}) - -it('can handle empty guide', async () => { - const results = await parser({ - date, - content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) - }) - - expect(results).toEqual([]) -}) +const { parser, url } = require('./guidetnt.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +const timezone = require('dayjs/plugin/timezone') +require('dayjs/locale/fr') +dayjs.extend(customParseFormat) +dayjs.extend(utc) +dayjs.extend(timezone) + +const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'tf1', + xmltv_id: 'TF1.fr' +} + +it('can generate valid url', () => { + expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1') +}) + +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = await parser({ content, date }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(29) + expect(results[0]).toMatchObject({ + category: 'Série', + description: + "Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...", + start: '2025-06-30T22:55:00.000Z', + stop: '2025-06-30T23:45:00.000Z', + title: 'Camping Paradis' + }) + expect(results[2]).toMatchObject({ + category: 'Magazine', + description: 'Retrouvez tous vos programmes de nuit.', + start: '2025-07-01T00:55:00.000Z', + stop: '2025-07-01T04:00:00.000Z', + title: 'Programmes de la nuit' + }) + expect(results[15]).toMatchObject({ + category: 'Téléfilm', + description: + "La vie quasi parfaite de Riley bascule brutalement lorsqu'un accident de voiture lui coûte la vie, laissant derrière elle sa famille. Alors que l'enquête débute, l'affaire prend une tournure étrange l...", + start: '2025-07-01T12:25:00.000Z', + stop: '2025-07-01T14:00:00.000Z', + title: "Trahie par l'amour" + }) +}) + +it('can parse response for current day', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html')) + let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') }) + results = results.map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(29) + expect(results[0]).toMatchObject({ + category: 'Série', + description: + "Grande effervescence pour toute l'équipe du Camping Paradis, qui prépare les Olympiades. Côté arrivants, Hélène et sa fille Eva viennent passer quelques jours dans le but d'optimiser les révisions d'E...", + start: '2025-06-30T22:55:00.000Z', + stop: '2025-06-30T23:45:00.000Z', + title: 'Camping Paradis' + }) +}) + +it('can handle empty guide', async () => { + const results = await parser({ + date, + content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) + }) + + expect(results).toEqual([]) +}) diff --git a/sites/tataplay.com/tataplay.com.config.js b/sites/tataplay.com/tataplay.com.config.js index 6cf3fda0..777634d8 100644 --- a/sites/tataplay.com/tataplay.com.config.js +++ b/sites/tataplay.com/tataplay.com.config.js @@ -1,78 +1,82 @@ -const axios = require('axios') - -module.exports = { - site: 'tataplay.com', - days: 1, - - url({ date }) { - return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format('DD-MM-YYYY')}` - }, - - request: { - method: 'POST', - headers: { - 'Accept': '*/*', - 'Origin': 'https://watch.tataplay.com', - 'Referer': 'https://watch.tataplay.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'content-type': 'application/json', - 'locale': 'ENG', - 'platform': 'web' - }, - data({ channel }) { - return { id: channel.site_id } - } - }, - - parser(context) { - let data = [] - try { - const json = JSON.parse(context.content) - const programs = json?.data?.epg || [] - - data = programs.map(program => ({ - title: program.title, - start: program.startTime, - stop: program.endTime, - description: program.desc, - category: program.category, - icon: program.boxCoverImage - })) - } catch { - data = [] - } - return data - }, - - async channels() { - const headers = { - 'Accept': '*/*', - 'Origin': 'https://watch.tataplay.com', - 'Referer': 'https://watch.tataplay.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'content-type': 'application/json', - 'locale': 'ENG', - 'platform': 'web' - } - - const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule' - const initialUrl = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=0` - const initialResponse = await axios.get(initialUrl, { headers }) - const total = initialResponse.data?.data?.total || 0 - const channels = [] - - for (let offset = 0; offset < total; offset += 20) { - const url = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=${offset}` - const response = await axios.get(url, { headers }) - const page = response.data?.data?.channelList || [] - channels.push(...page) - } - - return channels.map(channel => ({ - site_id: channel.id, - name: channel.title, - lang: 'en', - icon: channel.transparentImageUrl || channel.thumbnailImage - })) - } -} +const axios = require('axios') + +module.exports = { + site: 'tataplay.com', + days: 1, + + url({ date }) { + return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format( + 'DD-MM-YYYY' + )}` + }, + + request: { + method: 'POST', + headers: { + Accept: '*/*', + Origin: 'https://watch.tataplay.com', + Referer: 'https://watch.tataplay.com/', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'content-type': 'application/json', + locale: 'ENG', + platform: 'web' + }, + data({ channel }) { + return { id: channel.site_id } + } + }, + + parser(context) { + let data = [] + try { + const json = JSON.parse(context.content) + const programs = json?.data?.epg || [] + + data = programs.map(program => ({ + title: program.title, + start: program.startTime, + stop: program.endTime, + description: program.desc, + category: program.category, + icon: program.boxCoverImage + })) + } catch { + data = [] + } + return data + }, + + async channels() { + const headers = { + Accept: '*/*', + Origin: 'https://watch.tataplay.com', + Referer: 'https://watch.tataplay.com/', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'content-type': 'application/json', + locale: 'ENG', + platform: 'web' + } + + const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule' + const initialUrl = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=0` + const initialResponse = await axios.get(initialUrl, { headers }) + const total = initialResponse.data?.data?.total || 0 + const channels = [] + + for (let offset = 0; offset < total; offset += 20) { + const url = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=${offset}` + const response = await axios.get(url, { headers }) + const page = response.data?.data?.channelList || [] + channels.push(...page) + } + + return channels.map(channel => ({ + site_id: channel.id, + name: channel.title, + lang: 'en', + icon: channel.transparentImageUrl || channel.thumbnailImage + })) + } +} diff --git a/sites/tataplay.com/tataplay.com.test.js b/sites/tataplay.com/tataplay.com.test.js index b7adff1c..2f5551af 100644 --- a/sites/tataplay.com/tataplay.com.test.js +++ b/sites/tataplay.com/tataplay.com.test.js @@ -1,87 +1,89 @@ -const { parser, url, channels } = require('./tataplay.com.config.js') -const fs = require('fs') -const path = require('path') -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const customParseFormat = require('dayjs/plugin/customParseFormat') -dayjs.extend(customParseFormat) -dayjs.extend(utc) - -const date = dayjs.utc('2025-06-09', 'YYYY-MM-DD').startOf('d') -const channel = { site_id: '1001' } - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=09-06-2025') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - - const results = parser({ content, date }) - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject({ - title: 'Yeh Rishta Kya Kehlata Hai', - start: '2025-06-09T18:00:00.000Z', - stop: '2025-06-09T18:30:00.000Z', - description: 'The story of the Rajshri family and their journey through life.', - category: 'Drama', - icon: 'https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg' - }) - expect(results[1]).toMatchObject({ - title: 'Anupamaa', - start: '2025-06-09T18:30:00.000Z', - stop: '2025-06-09T19:00:00.000Z', - description: 'The story of Anupamaa, a housewife who rediscovers herself.', - category: 'Drama', - icon: 'https://img.tataplay.com/thumbnails/1001/anupamaa.jpg' - }) -}) - -it('can handle empty guide', () => { - const content = JSON.stringify({ data: { epg: [] } }) - const results = parser({ content, date }) - expect(results).toMatchObject([]) -}) - -it('can parse channel list', async () => { - const mockResponse = { - data: { - data: { - total: 2, - channelList: [ - { - id: '1001', - title: 'Star Plus', - transparentImageUrl: 'https://img.tataplay.com/channels/1001/logo.png' - }, - { - id: '1002', - title: 'Sony TV', - transparentImageUrl: 'https://img.tataplay.com/channels/1002/logo.png' - } - ] - } - } - } - - // Mock axios.get to return our test data - const axios = require('axios') - axios.get = jest.fn().mockResolvedValue(mockResponse) - - const results = await channels() - - expect(results.length).toBe(2) - expect(results[0]).toMatchObject({ - site_id: '1001', - name: 'Star Plus', - lang: 'en', - icon: 'https://img.tataplay.com/channels/1001/logo.png' - }) - expect(results[1]).toMatchObject({ - site_id: '1002', - name: 'Sony TV', - lang: 'en', - icon: 'https://img.tataplay.com/channels/1002/logo.png' - }) -}) \ No newline at end of file +const { parser, url, channels } = require('./tataplay.com.config.js') +const fs = require('fs') +const path = require('path') +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const customParseFormat = require('dayjs/plugin/customParseFormat') +dayjs.extend(customParseFormat) +dayjs.extend(utc) + +const date = dayjs.utc('2025-06-09', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '1001' } + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe( + 'https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=09-06-2025' + ) +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + + const results = parser({ content, date }) + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject({ + title: 'Yeh Rishta Kya Kehlata Hai', + start: '2025-06-09T18:00:00.000Z', + stop: '2025-06-09T18:30:00.000Z', + description: 'The story of the Rajshri family and their journey through life.', + category: 'Drama', + icon: 'https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg' + }) + expect(results[1]).toMatchObject({ + title: 'Anupamaa', + start: '2025-06-09T18:30:00.000Z', + stop: '2025-06-09T19:00:00.000Z', + description: 'The story of Anupamaa, a housewife who rediscovers herself.', + category: 'Drama', + icon: 'https://img.tataplay.com/thumbnails/1001/anupamaa.jpg' + }) +}) + +it('can handle empty guide', () => { + const content = JSON.stringify({ data: { epg: [] } }) + const results = parser({ content, date }) + expect(results).toMatchObject([]) +}) + +it('can parse channel list', async () => { + const mockResponse = { + data: { + data: { + total: 2, + channelList: [ + { + id: '1001', + title: 'Star Plus', + transparentImageUrl: 'https://img.tataplay.com/channels/1001/logo.png' + }, + { + id: '1002', + title: 'Sony TV', + transparentImageUrl: 'https://img.tataplay.com/channels/1002/logo.png' + } + ] + } + } + } + + // Mock axios.get to return our test data + const axios = require('axios') + axios.get = jest.fn().mockResolvedValue(mockResponse) + + const results = await channels() + + expect(results.length).toBe(2) + expect(results[0]).toMatchObject({ + site_id: '1001', + name: 'Star Plus', + lang: 'en', + icon: 'https://img.tataplay.com/channels/1001/logo.png' + }) + expect(results[1]).toMatchObject({ + site_id: '1002', + name: 'Sony TV', + lang: 'en', + icon: 'https://img.tataplay.com/channels/1002/logo.png' + }) +})