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

@@ -10,7 +10,8 @@
* 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. * Sorts an array by multiple criteria with customizable sort orders.
@@ -26,10 +27,19 @@ export const sortBy = <T>(arr: T[], fn: (item: T) => number | string): T[] => [.
* orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']); * orderBy(users, [x => x.age, x => x.name], ['desc', 'asc']);
* // [{name: 'bob', age: 30}, {name: 'john', age: 30}, {name: 'jane', age: 25}] * // [{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) => export const orderBy = (
fns.reduce((acc, fn, i) => arr: Array<unknown>,
acc || ((orders[i] === 'desc' ? fn(b) > fn(a) : fn(a) > fn(b)) ? 1 : fn(a) === fn(b) ? 0 : -1), 0) fns: Array<(item: unknown) => string | number>,
) orders: Array<string> = []
): Array<unknown> =>
[...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 * Creates a duplicate-free version of an array using an iteratee function to generate
@@ -44,7 +54,8 @@ export const orderBy = (arr: Array<unknown>, fns: Array<(item: unknown) => strin
* const users = [{id: 1, name: 'john'}, {id: 2, name: 'jane'}, {id: 1, name: 'john'}]; * 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'}] * uniqBy(users, x => x.id); // [{id: 1, name: 'john'}, {id: 2, name: 'jane'}]
*/ */
export const uniqBy = <T>(arr: T[], fn: (item: T) => unknown): T[] => arr.filter((item, index) => arr.findIndex(x => fn(x) === fn(item)) === index) export const uniqBy = <T>(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). * Converts a string to start case (capitalizes the first letter of each word).
@@ -59,7 +70,8 @@ export const uniqBy = <T>(arr: T[], fn: (item: T) => unknown): T[] => arr.filter
* startCase('hello-world'); // "Hello World" * startCase('hello-world'); // "Hello World"
* startCase('hello world'); // "Hello World" * startCase('hello world'); // "Hello World"
*/ */
export const startCase = (str: string): string => str export const startCase = (str: string): string =>
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase str
.replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces .replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase
.replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word .replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces
.replace(/\b\w/g, c => c.toUpperCase()) // Capitalize first letter of each word

View File

@@ -19,11 +19,11 @@ module.exports = {
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 }) {
@@ -57,8 +57,8 @@ module.exports = {
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)
@@ -66,21 +66,21 @@ module.exports = {
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,
@@ -110,115 +110,120 @@ module.exports = {
// 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) { function parseTimeRange(timeRange, baseDate) {
// Split times // Split times
const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim()) const [startStr, endStr] = timeRange.split(' - ').map(s => s.trim())
// Parse with base date // Parse with base date
const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm') const start = dayjs(`${baseDate} ${startStr}`, 'YYYY-MM-DD HH:mm')
let end = dayjs(`${baseDate} ${endStr}`, '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) // Handle possible day wrap (e.g., 23:30 - 00:15)
if (end.isBefore(start)) { if (end.isBefore(start)) {
end = end.add(1, 'day') end = end.add(1, 'day')
} }
// Calculate duration in minutes // Calculate duration in minutes
const diffMinutes = end.diff(start, 'minute') const diffMinutes = end.diff(start, 'minute')
return { return {
start: start.format(), start: start.format(),
stop: end.format(), stop: end.format(),
duration: diffMinutes duration: diffMinutes
} }
} }
function parseItemDetails(itemDetails) { function parseItemDetails(itemDetails) {
const $ = cheerio.load(itemDetails) const $ = cheerio.load(itemDetails)
const program = $('.program-wrapper').first() const program = $('.program-wrapper').first()
const programHour = program.find('.program-hour').text().trim() const programHour = program.find('.program-hour').text().trim()
const programTitle = program.find('.program-title').text().trim() const programTitle = program.find('.program-title').text().trim()
const programElementBold = program.find('.program-element-bold').text().trim() const programElementBold = program.find('.program-element-bold').text().trim()
const programArea1 = program.find('.program-element.program-area-1').text().trim() const programArea1 = program.find('.program-element.program-area-1').text().trim()
let description = '' let description = ''
const programElements = $('.program-element').filter((i, el) => { const programElements = $('.program-element').filter((i, el) => {
const classAttr = $(el).attr('class') const classAttr = $(el).attr('class')
// Return true only if it is exactly "program-element" (no extra classes) // Return true only if it is exactly "program-element" (no extra classes)
return classAttr.trim() === 'program-element' return classAttr.trim() === 'program-element'
}) })
programElements.each((i, el) => { programElements.each((i, el) => {
description += $(el).text().trim() description += $(el).text().trim()
}) })
const area2Node = $('.program-area-2').first() const area2Node = $('.program-area-2').first()
const area2 = $(area2Node) const area2 = $(area2Node)
const data = {} const data = {}
let currentLabel = null let currentLabel = null
let texts = [] let texts = []
area2.contents().each((i, node) => { area2.contents().each((i, node) => {
if (node.type === 'tag' && node.name === 'strong') { if (node.type === 'tag' && node.name === 'strong') {
// If we had collected some text for the previous label, save it // If we had collected some text for the previous label, save it
if (currentLabel && texts.length) { if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') // Remove trailing comma
} }
// New label - get text without colon // New label - get text without colon
currentLabel = $(node).text().replace(/:$/, '').trim() currentLabel = $(node).text().replace(/:$/, '').trim()
texts = [] texts = []
} else if (currentLabel) { } else if (currentLabel) {
// Append the text content (text node or others) // Append the text content (text node or others)
if (node.type === 'text') { if (node.type === 'text') {
texts.push(node.data) texts.push(node.data)
} else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') { } else if (node.type === 'tag' && node.name !== 'strong' && node.name !== 'br') {
texts.push($(node).text()) texts.push($(node).text())
} }
} }
}) })
// Save last label text // Save last label text
if (currentLabel && texts.length) { if (currentLabel && texts.length) {
data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '') data[currentLabel] = texts.join('').trim().replace(/,\s*$/, '')
} }
const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null const imgSrc = program.find('div[style*="float:left"]')?.find('img')?.attr('src') || null
return { return {
programHour, programHour,
title: programTitle, title: programTitle,
subTitle: programElementBold, subTitle: programElementBold,
category: programArea1, category: programArea1,
description: description, description: description,
directorActors: data, directorActors: data,
image: imgSrc image: imgSrc
} }
} }
function parseCategoryText(text) { function parseCategoryText(text) {
if (!text) return null if (!text) return null
const parts = text.split(',').map(s => s.trim()).filter(Boolean) const parts = text
.split(',')
.map(s => s.trim())
.filter(Boolean)
const len = parts.length const len = parts.length
const category = parts[0] || null const category = parts[0] || null
@@ -252,18 +257,18 @@ function parseCategoryText(text) {
//duration = [{ units: 'minutes', value: durationMinute }], //duration = [{ units: 'minutes', value: durationMinute }],
durationIndex = i durationIndex = i
} else if (parts[i].toLowerCase().includes('épisode')) { } else if (parts[i].toLowerCase().includes('épisode')) {
const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i) const match = text.match(/épisode\s+(\d+)(?:\/(\d+))?/i)
if (match) { if (match) {
episode = parseInt(match[1], 10) episode = parseInt(match[1], 10)
} }
} else if (parts[i].toLowerCase().includes('saison')) { } else if (parts[i].toLowerCase().includes('saison')) {
season = parts[i].replace('saison', '').trim() season = parts[i].replace('saison', '').trim()
} }
} }
// Country: second to last // Country: second to last
const countryIndex = len - 2 const countryIndex = len - 2
let country = (durationIndex === countryIndex) ? null : parts[countryIndex] let country = durationIndex === countryIndex ? null : parts[countryIndex]
return { return {
category, category,
@@ -325,9 +330,9 @@ function parseItems(content) {
const rows = $('.channel-row').toArray() const rows = $('.channel-row').toArray()
return { return {
rows, rows,
logoSrc, logoSrc,
title, title,
formattedDate formattedDate
} }
} }

View File

@@ -32,7 +32,8 @@ it('can parse response', async () => {
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:
"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', start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z', stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis' title: 'Camping Paradis'
@@ -45,11 +46,12 @@ it('can parse response', async () => {
title: 'Programmes de la nuit' title: 'Programmes de la nuit'
}) })
expect(results[15]).toMatchObject({ expect(results[15]).toMatchObject({
category: 'Téléfilm', 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...', 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', start: '2025-07-01T12:25:00.000Z',
stop: '2025-07-01T14:00:00.000Z', stop: '2025-07-01T14:00:00.000Z',
title: 'Trahie par l\'amour' title: "Trahie par l'amour"
}) })
}) })
@@ -57,16 +59,16 @@ it('can parse response for current day', 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: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') }) let results = await parser({ content, date: dayjs.utc('2025-07-01', 'YYYY-MM-DD').startOf('d') })
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:
"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', start: '2025-06-30T22:55:00.000Z',
stop: '2025-06-30T23:45:00.000Z', stop: '2025-06-30T23:45:00.000Z',
title: 'Camping Paradis' title: 'Camping Paradis'

View File

@@ -5,19 +5,22 @@ module.exports = {
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: { request: {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': '*/*', Accept: '*/*',
'Origin': 'https://watch.tataplay.com', Origin: 'https://watch.tataplay.com',
'Referer': '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', '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', 'content-type': 'application/json',
'locale': 'ENG', locale: 'ENG',
'platform': 'web' platform: 'web'
}, },
data({ channel }) { data({ channel }) {
return { id: channel.site_id } return { id: channel.site_id }
@@ -46,13 +49,14 @@ module.exports = {
async channels() { async channels() {
const headers = { const headers = {
'Accept': '*/*', Accept: '*/*',
'Origin': 'https://watch.tataplay.com', Origin: 'https://watch.tataplay.com',
'Referer': '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', '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', 'content-type': 'application/json',
'locale': 'ENG', locale: 'ENG',
'platform': 'web' platform: 'web'
} }
const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule' const baseUrl = 'https://tm.tapi.videoready.tv/portal-search/pub/api/v1/channels/schedule'

View File

@@ -11,7 +11,9 @@ 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', () => { it('can parse response', () => {