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,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
}
}
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
}
}

View File

@@ -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([])
})

View File

@@ -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
}))
}
}

View File

@@ -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'
})
})
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'
})
})