Fix linter issues

This commit is contained in:
freearhey
2025-07-28 04:07:40 +03:00
parent 851aba2438
commit 7afd3fe3fe
5 changed files with 671 additions and 646 deletions

View File

@@ -1,65 +1,77 @@
/** /**
* Sorts an array by the result of running each element through an iteratee function. * 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. * Creates a shallow copy of the array before sorting to avoid mutating the original.
* *
* @param {Array} arr - The array to sort * @param {Array} arr - The array to sort
* @param {Function} fn - The iteratee function to compute sort values * @param {Function} fn - The iteratee function to compute sort values
* @returns {Array} A new sorted array * @returns {Array} A new sorted array
* *
* @example * @example
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}]; * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}];
* sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}] * sortBy(users, x => x.age); // [{name: 'jane', age: 25}, {name: 'john', age: 30}]
*/ */
export const sortBy = <T>(arr: T[], fn: (item: T) => number | string): T[] => [...arr].sort((a, b) => fn(a) > fn(b) ? 1 : -1) export const sortBy = <T>(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. * 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<Function>} fns - Array of iteratee functions to compute sort values * @param {Array} arr - The array to sort
* @param {Array<string>} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc' * @param {Array<Function>} fns - Array of iteratee functions to compute sort values
* @returns {Array} A new sorted array * @param {Array<string>} orders - Array of sort orders ('asc' or 'desc'), defaults to all 'asc'
* * @returns {Array} A new sorted array
* @example *
* const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}]; * @example
* orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']); * const users = [{name: 'john', age: 30}, {name: 'jane', age: 25}, {name: 'bob', age: 30}];
* // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}] * 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<unknown>, fns: Array<(item: unknown) => string | number>, orders: Array<string> = []): Array<unknown> => [...arr].sort((a, b) => */
fns.reduce((acc, fn, i) => export const orderBy = (
acc || ((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1), 0) arr: Array<unknown>,
) fns: Array<(item: unknown) => string | number>,
orders: Array<string> = []
/** ): Array<unknown> =>
* Creates a duplicate-free version of an array using an iteratee function to generate [...arr].sort((a, b) =>
* the criterion by which uniqueness is computed. Only the first occurrence of each fns.reduce(
* element is kept. (acc, fn, i) =>
* acc ||
* @param {Array} arr - The array to inspect ((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1),
* @param {Function} fn - The iteratee function to compute uniqueness criterion 0
* @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'}] * 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
export const uniqBy = <T>(arr: T[], fn: (item: T) => unknown): T[] => arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index) * element is kept.
*
/** * @param {Array} arr - The array to inspect
* Converts a string to start case (capitalizes the first letter of each word). * @param {Function} fn - The iteratee function to compute uniqueness criterion
* Handles camelCase, snake_case, kebab-case, and regular spaces. * @returns {Array} A new duplicate-free array
* *
* @param {string} str - The string to convert * @example
* @returns {string} The start case string * 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'}]
* @example */
* startCase('hello_world'); // "Hello World" export const uniqBy = <T>(arr: T[], fn: (item: T) => unknown): T[] =>
* startCase('helloWorld'); // "Hello World" arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index)
* startCase('hello-world'); // "Hello World"
* startCase('hello world'); // "Hello World" /**
*/ * Converts a string to start case (capitalizes the first letter of each word).
export const startCase = (str: string): string => str * Handles camelCase, snake_case, kebab-case, and regular spaces.
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase *
.replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces * @param {string} str - The string to convert
.replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word * @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

View File

@@ -1,333 +1,338 @@
const cheerio = require('cheerio') const cheerio = require('cheerio')
const axios = require('axios') const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
require('dayjs/locale/fr') require('dayjs/locale/fr')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const PARIS_TZ = 'Europe/Paris' const PARIS_TZ = 'Europe/Paris'
module.exports = { module.exports = {
site: 'guidetnt.com', site: 'guidetnt.com',
days: 2, days: 2,
url({ channel, date }) { url({ channel, date }) {
const now = dayjs() const now = dayjs()
const demain = now.add(1, 'd') const demain = now.add(1, 'd')
if (date && date.isSame(demain, 'day')) { if (date && date.isSame(demain, 'day')) {
return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}` return `https://www.guidetnt.com/tv-demain/programme-${channel.site_id}`
} else if (!date || date.isSame(now, 'day')) { } else if (!date || date.isSame(now, 'day')) {
return `https://www.guidetnt.com/tv/programme-${channel.site_id}` return `https://www.guidetnt.com/tv/programme-${channel.site_id}`
} else { } else {
return null return null
} }
}, },
async parser({ content, date }) { async parser({ content, date }) {
const programs = [] const programs = []
const allItems = parseItems(content) const allItems = parseItems(content)
const items = allItems?.rows const items = allItems?.rows
const itemDate = allItems?.formattedDate const itemDate = allItems?.formattedDate
for (const item of items) { for (const item of items) {
const prev = programs[programs.length - 1] const prev = programs[programs.length - 1]
const $item = cheerio.load(item) const $item = cheerio.load(item)
const title = parseTitle($item) const title = parseTitle($item)
let start = parseStart($item, itemDate) let start = parseStart($item, itemDate)
if (!start || !title) return if (!start || !title) return
if (prev) { if (prev) {
if (start.isBefore(prev.start)) { if (start.isBefore(prev.start)) {
start = start.add(1, 'd') start = start.add(1, 'd')
date = date.add(1, 'd') date = date.add(1, 'd')
} }
prev.stop = start prev.stop = start
} }
let stop = start.add(30, 'm') let stop = start.add(30, 'm')
let itemDetails = null let itemDetails = null
let subTitle = null let subTitle = null
//let duration = null //let duration = null
let country = null let country = null
let productionDate = null let productionDate = null
let episode = null let episode = null
let season = null let season = null
let category = parseCategory($item) let category = parseCategory($item)
let description = parseDescription($item) let description = parseDescription($item)
const itemDetailsURL = parseDescriptionURL($item) const itemDetailsURL = parseDescriptionURL($item)
if(itemDetailsURL) { if (itemDetailsURL) {
const url = 'https://www.guidetnt.com' + itemDetailsURL const url = 'https://www.guidetnt.com' + itemDetailsURL
try { try {
const response = await axios.get(url) const response = await axios.get(url)
itemDetails = parseItemDetails(response.data) itemDetails = parseItemDetails(response.data)
} catch (err) { } catch (err) {
console.error(`Erreur lors du fetch des détails pour l'item: ${url}`, 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')) const timeRange = parseTimeRange(itemDetails?.programHour, date.format('YYYY-MM-DD'))
start = timeRange?.start start = timeRange?.start
stop = timeRange?.stop stop = timeRange?.stop
subTitle = itemDetails?.subTitle subTitle = itemDetails?.subTitle
if (title == subTitle) subTitle = null if (title == subTitle) subTitle = null
description = itemDetails?.description description = itemDetails?.description
const categoryDetails = parseCategoryText(itemDetails?.category) const categoryDetails = parseCategoryText(itemDetails?.category)
//duration = categoryDetails?.duration //duration = categoryDetails?.duration
country = categoryDetails?.country country = categoryDetails?.country
productionDate = categoryDetails?.productionDate productionDate = categoryDetails?.productionDate
season = categoryDetails?.season season = categoryDetails?.season
episode = categoryDetails?.episode episode = categoryDetails?.episode
} }
// See https://www.npmjs.com/package/epg-parser for parameters // See https://www.npmjs.com/package/epg-parser for parameters
programs.push({ programs.push({
title, title,
subTitle: subTitle, subTitle: subTitle,
description: description, description: description,
image: itemDetails?.image, image: itemDetails?.image,
category: category, category: category,
directors: itemDetails?.directorActors?.Réalisateur, directors: itemDetails?.directorActors?.Réalisateur,
actors: itemDetails?.directorActors?.Acteur, actors: itemDetails?.directorActors?.Acteur,
country: country, country: country,
date: productionDate, date: productionDate,
//duration: duration, // Tried with length: too, but does not work ! (stop-start is not accurate because of Ads) //duration: duration, // Tried with length: too, but does not work ! (stop-start is not accurate because of Ads)
season: season, season: season,
episode: episode, episode: episode,
start, start,
stop stop
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
const response = await axios.get('https://www.guidetnt.com') const response = await axios.get('https://www.guidetnt.com')
const channels = [] const channels = []
const $ = cheerio.load(response.data) const $ = cheerio.load(response.data)
// Look inside each .tvlogo container // Look inside each .tvlogo container
$('.tvlogo').each((i, el) => { $('.tvlogo').each((i, el) => {
// Find all descendants that have an alt attribute // Find all descendants that have an alt attribute
$(el).find('[alt]').each((j, subEl) => { $(el)
const alt = $(subEl).attr('alt') .find('[alt]')
const href = $(subEl).attr('href') .each((j, subEl) => {
if (href && alt && alt.trim() !== '') { const alt = $(subEl).attr('alt')
const name = alt.trim() const href = $(subEl).attr('href')
const site_id = href.replace(/^\/tv\/programme-/, '') if (href && alt && alt.trim() !== '') {
channels.push({ const name = alt.trim()
lang: 'fr', const site_id = href.replace(/^\/tv\/programme-/, '')
name, channels.push({
site_id lang: 'fr',
}) name,
} site_id
}) })
}) }
return channels })
} })
} return channels
}
function parseTimeRange(timeRange, baseDate) { }
// Split times
const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim()) function parseTimeRange(timeRange, baseDate) {
// Split times
// Parse with base date const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim())
const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm')
let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm') // Parse with base date
const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm')
// Handle possible day wrap (e.g., 23:30 - 00:15) let end = dayjs(`${baseDate} ${endStr}`, 'YYYY-MM-DD HH:mm')
if (end.isBefore(start)) {
end = end.add(1, 'day') // 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')
// Calculate duration in minutes
return { const diffMinutes = end.diff(start, 'minute')
start: start.format(),
stop: end.format(), return {
duration: diffMinutes start: start.format(),
} stop: end.format(),
} duration: diffMinutes
}
function parseItemDetails(itemDetails) { }
const $ = cheerio.load(itemDetails)
function parseItemDetails(itemDetails) {
const program = $('.program-wrapper').first() const $ = cheerio.load(itemDetails)
const programHour = program.find('.program-hour').text().trim() const program = $('.program-wrapper').first()
const programTitle = program.find('.program-title').text().trim()
const programElementBold = program.find('.program-element-bold').text().trim() const programHour = program.find('.program-hour').text().trim()
const programArea1 = program.find('.program-element.program-area-1').text().trim() const programTitle = program.find('.program-title').text().trim()
const programElementBold = program.find('.program-element-bold').text().trim()
let description = '' const programArea1 = program.find('.program-element.program-area-1').text().trim()
const programElements = $('.program-element').filter((i, el) => {
const classAttr = $(el).attr('class') let description = ''
// Return true only if it is exactly "program-element" (no extra classes) const programElements = $('.program-element').filter((i, el) => {
return classAttr.trim() === 'program-element' 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()
}) programElements.each((i, el) => {
description += $(el).text().trim()
const area2Node = $('.program-area-2').first() })
const area2 = $(area2Node)
const data = {} const area2Node = $('.program-area-2').first()
let currentLabel = null const area2 = $(area2Node)
let texts = [] const data = {}
let currentLabel = null
area2.contents().each((i, node) => { let texts = []
if (node.type === 'tag' && node.name === 'strong') {
// If we had collected some text for the previous label, save it area2.contents().each((i, node) => {
if (currentLabel && texts.length) { if (node.type === 'tag' && node.name === 'strong') {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma // If we had collected some text for the previous label, save it
} if (currentLabel && texts.length) {
// New label - get text without colon data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma
currentLabel = $(node).text().replace(/:$/, '').trim() }
texts = [] // New label - get text without colon
} else if (currentLabel) { currentLabel = $(node).text().replace(/:$/, '').trim()
// Append the text content (text node or others) texts = []
if (node.type === 'text') { } else if (currentLabel) {
texts.push(node.data) // Append the text content (text node or others)
} else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') { if (node.type === 'text') {
texts.push($(node).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*$/, '') // 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 { const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null
programHour,
title: programTitle, return {
subTitle: programElementBold, programHour,
category: programArea1, title: programTitle,
description: description, subTitle: programElementBold,
directorActors: data, category: programArea1,
image: imgSrc description: description,
} directorActors: data,
} image: imgSrc
}
function parseCategoryText(text) { }
if (!text) return null
function parseCategoryText(text) {
const parts = text.split(',').map(s => s.trim()).filter(Boolean) if (!text) return null
const len = parts.length
const parts = text
const category = parts[0] || null .split(',')
.map(s => s.trim())
if (len < 3) { .filter(Boolean)
return { const len = parts.length
category: category,
duration: null, const category = parts[0] || null
country: null,
productionDate: null, if (len < 3) {
season: null, return {
episode: null category: category,
} duration: null,
} country: null,
productionDate: null,
// Check last part: date if numeric season: null,
const dateCandidate = parts[len - 1] episode: null
const productionDate = /^\d{4}$/.test(dateCandidate) ? dateCandidate : null }
}
// Check for duration (first part containing "minutes")
let durationMinute = null // Check last part: date if numeric
//let duration = null const dateCandidate = parts[len - 1]
let episode = null const productionDate = /^\d{4}$/.test(dateCandidate) ? dateCandidate : null
let season = null
let durationIndex = -1 // Check for duration (first part containing "minutes")
for (let i = 0; i < len; i++) { let durationMinute = null
if (parts[i].toLowerCase().includes('minute')) { //let duration = null
durationMinute = parts[i].trim() let episode = null
durationMinute = durationMinute.replace('minutes', '') let season = null
durationMinute = durationMinute.replace('minute', '') let durationIndex = -1
//duration = [{ units: 'minutes', value: durationMinute }], for (let i = 0; i < len; i++) {
durationIndex = i if (parts[i].toLowerCase().includes('minute')) {
} else if (parts[i].toLowerCase().includes('épisode')) { durationMinute = parts[i].trim()
const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i) durationMinute = durationMinute.replace('minutes', '')
if (match) { durationMinute = durationMinute.replace('minute', '')
episode = parseInt(match[1], 10) //duration = [{ units: 'minutes', value: durationMinute }],
} durationIndex = i
} else if (parts[i].toLowerCase().includes('saison')) { } else if (parts[i].toLowerCase().includes('épisode')) {
season = parts[i].replace('saison', '').trim() const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i)
} if (match) {
} episode = parseInt(match[1], 10)
}
// Country: second to last } else if (parts[i].toLowerCase().includes('saison')) {
const countryIndex = len - 2 season = parts[i].replace('saison', '').trim()
let country = (durationIndex === countryIndex) ? null : parts[countryIndex] }
}
return {
category, // Country: second to last
durationMinute, const countryIndex = len - 2
country, let country = durationIndex === countryIndex ? null : parts[countryIndex]
productionDate,
season, return {
episode category,
} durationMinute,
} country,
productionDate,
function parseTitle($item) { season,
return $item('.channel-programs-title a').text().trim() episode
} }
}
function parseDescription($item) {
return $item('#descr').text().trim() || null function parseTitle($item) {
} return $item('.channel-programs-title a').text().trim()
}
function parseDescriptionURL($item) {
const descrLink = $item('#descr a') function parseDescription($item) {
return descrLink.attr('href') || null return $item('#descr').text().trim() || null
} }
function parseCategory($item) { function parseDescriptionURL($item) {
let type = null const descrLink = $item('#descr a')
$item('.channel-programs-title span').each((i, span) => { return descrLink.attr('href') || null
const className = $item(span).attr('class') }
if (className && className.startsWith('text_bg')) {
type = $item(span).text().trim() function parseCategory($item) {
} let type = null
}) $item('.channel-programs-title span').each((i, span) => {
return type const className = $item(span).attr('class')
} if (className && className.startsWith('text_bg')) {
type = $item(span).text().trim()
function parseStart($item, itemDate) { }
const dt = $item('.channel-programs-time a').text().trim() })
if (!dt) return null return type
}
const datetimeStr = `${itemDate} ${dt}`
return dayjs.tz(datetimeStr, 'YYYY-MM-DD HH:mm', PARIS_TZ) function parseStart($item, itemDate) {
} const dt = $item('.channel-programs-time a').text().trim()
if (!dt) return null
function parseItems(content) {
const $ = cheerio.load(content) const datetimeStr = `${itemDate} ${dt}`
return dayjs.tz(datetimeStr, 'YYYY-MM-DD HH:mm', PARIS_TZ)
// Extract header information }
const logoSrc = $('#logo img').attr('src')
const title = $('#title h1').text().trim() function parseItems(content) {
const subtitle = $('#subtitle').text().trim() const $ = cheerio.load(content)
const dateMatch = subtitle.match(/(\d{1,2} \w+ \d{4})/)
const dateStr = dateMatch ? dateMatch[1].toLowerCase() : null // Extract header information
const logoSrc = $('#logo img').attr('src')
// Parse the French date string const title = $('#title h1').text().trim()
const parsedDate = dayjs(dateStr, 'D MMMM YYYY', 'fr') const subtitle = $('#subtitle').text().trim()
// Format it as YYYY-MM-DD const dateMatch = subtitle.match(/(\d{1,2} \w+ \d{4})/)
const formattedDate = parsedDate.format('YYYY-MM-DD') const dateStr = dateMatch ? dateMatch[1].toLowerCase() : null
const rows = $('.channel-row').toArray() // Parse the French date string
const parsedDate = dayjs(dateStr, 'D MMMM YYYY', 'fr')
return { // Format it as YYYY-MM-DD
rows, const formattedDate = parsedDate.format('YYYY-MM-DD')
logoSrc,
title, const rows = $('.channel-row').toArray()
formattedDate
} return {
} rows,
logoSrc,
title,
formattedDate
}
}

View File

@@ -1,83 +1,85 @@
const { parser, url } = require('./guidetnt.com.config.js') const { parser, url } = require('./guidetnt.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')
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')
require('dayjs/locale/fr') require('dayjs/locale/fr')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'tf1', site_id: 'tf1',
xmltv_id: 'TF1.fr' xmltv_id: 'TF1.fr'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1') expect(url({ channel })).toBe('https://www.guidetnt.com/tv/programme-tf1')
}) })
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'))
let results = await parser({ content, date }) let results = await parser({ content, date })
results = results.map(p => { results = results.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
}) })
expect(results.length).toBe(29) expect(results.length).toBe(29)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
category: 'Série', 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...', description:
start: '2025-06-30T22:55:00.000Z', "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...",
stop: '2025-06-30T23:45:00.000Z', start: '2025-06-30T22:55:00.000Z',
title: 'Camping Paradis' stop: '2025-06-30T23:45:00.000Z',
}) title: 'Camping Paradis'
expect(results[2]).toMatchObject({ })
category: 'Magazine', expect(results[2]).toMatchObject({
description: 'Retrouvez tous vos programmes de nuit.', category: 'Magazine',
start: '2025-07-01T00:55:00.000Z', description: 'Retrouvez tous vos programmes de nuit.',
stop: '2025-07-01T04:00:00.000Z', start: '2025-07-01T00:55:00.000Z',
title: 'Programmes de la nuit' stop: '2025-07-01T04:00:00.000Z',
}) title: 'Programmes de la nuit'
expect(results[15]).toMatchObject({ })
category: 'Téléfilm', expect(results[15]).toMatchObject({
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...', category: 'Téléfilm',
start: '2025-07-01T12:25:00.000Z', description:
stop: '2025-07-01T14:00:00.000Z', "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...",
title: 'Trahie par l\'amour' 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') }) it('can parse response for current day', async () => {
results = results.map(p => { const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
p.start = p.start.toJSON() let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') })
p.stop = p.stop.toJSON() results = results.map(p => {
return p p.start = p.start.toJSON()
} p.stop = p.stop.toJSON()
) return p
})
expect(results.length).toBe(29)
expect(results[0]).toMatchObject({ expect(results.length).toBe(29)
category: 'Série', expect(results[0]).toMatchObject({
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...', category: 'Série',
start: '2025-06-30T22:55:00.000Z', description:
stop: '2025-06-30T23:45:00.000Z', "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...",
title: 'Camping Paradis' 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, it('can handle empty guide', async () => {
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html')) const results = await parser({
}) date,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
expect(results).toEqual([]) })
})
expect(results).toEqual([])
})

View File

@@ -1,78 +1,82 @@
const axios = require('axios') const axios = require('axios')
module.exports = { module.exports = {
site: 'tataplay.com', site: 'tataplay.com',
days: 1, days: 1,
url({ date }) { url({ date }) {
return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format('DD-MM-YYYY')}` return `https://tm.tapi.videoready.tv/content-detail/pub/api/v2/channels/schedule?date=${date.format(
}, 'DD-MM-YYYY'
)}`
request: { },
method: 'POST',
headers: { request: {
'Accept': '*/*', method: 'POST',
'Origin': 'https://watch.tataplay.com', headers: {
'Referer': 'https://watch.tataplay.com/', Accept: '*/*',
'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', Origin: 'https://watch.tataplay.com',
'content-type': 'application/json', Referer: 'https://watch.tataplay.com/',
'locale': 'ENG', 'User-Agent':
'platform': 'web' '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',
data({ channel }) { locale: 'ENG',
return { id: channel.site_id } platform: 'web'
} },
}, data({ channel }) {
return { id: channel.site_id }
parser(context) { }
let data = [] },
try {
const json = JSON.parse(context.content) parser(context) {
const programs = json?.data?.epg || [] let data = []
try {
data = programs.map(program => ({ const json = JSON.parse(context.content)
title: program.title, const programs = json?.data?.epg || []
start: program.startTime,
stop: program.endTime, data = programs.map(program => ({
description: program.desc, title: program.title,
category: program.category, start: program.startTime,
icon: program.boxCoverImage stop: program.endTime,
})) description: program.desc,
} catch { category: program.category,
data = [] icon: program.boxCoverImage
} }))
return data } catch {
}, data = []
}
async channels() { return data
const headers = { },
'Accept': '*/*',
'Origin': 'https://watch.tataplay.com', async channels() {
'Referer': 'https://watch.tataplay.com/', const headers = {
'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', Accept: '*/*',
'content-type': 'application/json', Origin: 'https://watch.tataplay.com',
'locale': 'ENG', Referer: 'https://watch.tataplay.com/',
'platform': 'web' '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',
const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule' locale: 'ENG',
const initialUrl = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=0` platform: 'web'
const initialResponse = await axios.get(initialUrl, { headers }) }
const total = initialResponse.data?.data?.total || 0
const channels = [] const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule'
const initialUrl = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=0`
for (let offset = 0; offset < total; offset += 20) { const initialResponse = await axios.get(initialUrl, { headers })
const url = `${baseUrl}?date=&languageFilters=&genreFilters=&limit=20&offset=${offset}` const total = initialResponse.data?.data?.total || 0
const response = await axios.get(url, { headers }) const channels = []
const page = response.data?.data?.channelList || []
channels.push(...page) 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 })
return channels.map(channel => ({ const page = response.data?.data?.channelList || []
site_id: channel.id, channels.push(...page)
name: channel.title, }
lang: 'en',
icon: channel.transparentImageUrl || channel.thumbnailImage return channels.map(channel => ({
})) site_id: channel.id,
} name: channel.title,
} lang: 'en',
icon: channel.transparentImageUrl || channel.thumbnailImage
}))
}
}

View File

@@ -1,87 +1,89 @@
const { parser, url, channels } = require('./tataplay.com.config.js') const { parser, url, channels } = require('./tataplay.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')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const date = dayjs.utc('2025-06-09', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-06-09', 'YYYY-MM-DD').startOf('d')
const channel = { site_id: '1001' } const channel = { site_id: '1001' }
it('can generate valid url', () => { 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') 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'))
it('can parse response', () => {
const results = parser({ content, date }) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
expect(results.length).toBe(2) const results = parser({ content, date })
expect(results[0]).toMatchObject({
title: 'Yeh Rishta Kya Kehlata Hai', expect(results.length).toBe(2)
start: '2025-06-09T18:00:00.000Z', expect(results[0]).toMatchObject({
stop: '2025-06-09T18:30:00.000Z', title: 'Yeh Rishta Kya Kehlata Hai',
description: 'The story of the Rajshri family and their journey through life.', start: '2025-06-09T18:00:00.000Z',
category: 'Drama', stop: '2025-06-09T18:30:00.000Z',
icon: 'https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg' description: 'The story of the Rajshri family and their journey through life.',
}) category: 'Drama',
expect(results[1]).toMatchObject({ icon: 'https://img.tataplay.com/thumbnails/1001/yeh-rishta.jpg'
title: 'Anupamaa', })
start: '2025-06-09T18:30:00.000Z', expect(results[1]).toMatchObject({
stop: '2025-06-09T19:00:00.000Z', title: 'Anupamaa',
description: 'The story of Anupamaa, a housewife who rediscovers herself.', start: '2025-06-09T18:30:00.000Z',
category: 'Drama', stop: '2025-06-09T19:00:00.000Z',
icon: 'https://img.tataplay.com/thumbnails/1001/anupamaa.jpg' 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 }) it('can handle empty guide', () => {
expect(results).toMatchObject([]) const content = JSON.stringify({ data: { epg: [] } })
}) const results = parser({ content, date })
expect(results).toMatchObject([])
it('can parse channel list', async () => { })
const mockResponse = {
data: { it('can parse channel list', async () => {
data: { const mockResponse = {
total: 2, data: {
channelList: [ data: {
{ total: 2,
id: '1001', channelList: [
title: 'Star Plus', {
transparentImageUrl: 'https://img.tataplay.com/channels/1001/logo.png' 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' 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) // Mock axios.get to return our test data
const axios = require('axios')
const results = await channels() axios.get = jest.fn().mockResolvedValue(mockResponse)
expect(results.length).toBe(2) const results = await channels()
expect(results[0]).toMatchObject({
site_id: '1001', expect(results.length).toBe(2)
name: 'Star Plus', expect(results[0]).toMatchObject({
lang: 'en', site_id: '1001',
icon: 'https://img.tataplay.com/channels/1001/logo.png' name: 'Star Plus',
}) lang: 'en',
expect(results[1]).toMatchObject({ icon: 'https://img.tataplay.com/channels/1001/logo.png'
site_id: '1002', })
name: 'Sony TV', expect(results[1]).toMatchObject({
lang: 'en', site_id: '1002',
icon: 'https://img.tataplay.com/channels/1002/logo.png' name: 'Sony TV',
}) lang: 'en',
}) icon: 'https://img.tataplay.com/channels/1002/logo.png'
})
})