Replace LF line endings with CRLF

This commit is contained in:
freearhey
2025-09-28 17:55:05 +03:00
parent efc74efcf8
commit b6a589c62a
1192 changed files with 445631 additions and 445631 deletions

View File

@@ -1,73 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="guidetnt.com" lang="fr" xmltv_id="TF1.fr" site_id="tf1">TF1</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France2.fr" site_id="france-2">France 2</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France3.fr" site_id="france-3">France 3</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France4.fr" site_id="france-4">France 4</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France5.fr" site_id="france-5">France 5</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="M6.fr" site_id="m6">M6</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="arte.fr" site_id="arte">Arte</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="W9.fr" site_id="w9">W9</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TMC.fr" site_id="tmc">TMC</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TFX.fr" site_id="tfx">TFX</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TF1SeriesFilms.fr" site_id="tf1-series-films">TF1 Séries Films</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CStar.fr" site_id="cstar">CSTAR</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Cherie25.fr" site_id="cherie-25">Chérie 25</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="T18.fr" site_id="t18">T18</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="6ter.fr" site_id="6ter">6ter</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="RMCStory.fr" site_id="rmc-story">RMC STORY</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TV5MondeFranceBelgiumSwitzerlandMonaco.fr" site_id="tv5monde">TV5MONDE</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="LCP.fr" site_id="lcp-public-senat">LCP / Public Senat</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Gulli.fr" site_id="gulli">Gulli</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="DisneyChannel.fr" site_id="disney-channel">Disney Channel</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusKids.fr" site_id="canalplus-kids">Canal+ Kids</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="AB1.fr" site_id="ab1">AB1</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="MTV.fr" site_id="mtv">MTV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="E.fr" site_id="e-entertainment">E! Entertainment</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="13emeRue.fr" site_id="13eme-rue">13ème RUE</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TVBreizh.fr" site_id="tv-breizh"></channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PolarPlus.fr" site_id="polarplus">Polar+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ComediePlus.fr" site_id="comedieplus">Comedie+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ComedyCentral.fr" site_id="comedy-central">Comedy Central</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="BET.fr" site_id="bet">BET</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="MCM.fr" site_id="mcm">MCM</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ParamountChannel.fr" site_id="paramount-channel">Paramount Channel</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="WarnerTV.fr" site_id="warner-tv">Warner TV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="RTL9.lu" site_id="rtl9">RTL9</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Syfy.fr" site_id="syfy">Syfy</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Action.fr" site_id="action">Action</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TCMCinema.fr" site_id="tcm">TCM</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlus.fr" site_id="canalplus">Canal+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusCinemas.fr" site_id="canalplus-cinema">Canal+ Cinéma(s)</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusGrandEcran.fr" site_id="canalplus-grand-ecran">Canal+ Grand écran</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusBoxOffice.fr" site_id="canalplus-box-office">Canal+ Box office</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusSeries.fr" site_id="canalplus-series">Canal+ Séries</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusDocs.fr" site_id="canalplus-docs">Canal+ Docs</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusOCS.fr" site_id="ocs">OCS</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusFrisson.fr" site_id="cineplus-frisson">Cine+ Frisson</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusClassic.fr" site_id="cineplus-classic">Cine+ Classic</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusFestival.fr" site_id="cineplus-festival">Cine+ Festival</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusEmotion.fr" site_id="cineplus-emotion">Cine+ Emotion</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusFamily.fr" site_id="cineplus-family">Cine+ Family</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="HistoireTV.fr" site_id="histoire">Histoire</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ToutelHistoire.fr" site_id="toute-l-histoire">Toute l&apos;histoire</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CrimeDistrict.fr" site_id="crime-district">Crime District</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="DiscoveryChannel.fr" site_id="discovery-channel">Discovery Channel</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ScienceVieTV.fr" site_id="science-vie-tv">Science&amp;Vie TV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="RMCDecouverte.fr" site_id="rmc-decouverte">RMC Découverte</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PlanetePlus.fr" site_id="planeteplus">Planète+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PlanetePlusCrime.fr" site_id="planeteplus-crime">Planète+ Crime</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PlanetePlusAventure.fr" site_id="planeteplus-aventure">Planète+ Aventure</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="NationalGeographic.fr" site_id="national-geographic">National Geographic</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="NationalGeographicWild.fr" site_id="nat-geo-wild">Nat Geo Wild</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Animaux.fr" site_id="animaux">Animaux</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="UshuaiaTV.fr" site_id="ushuaia-tv">Ushuaia TV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Trek.fr" site_id="trek">Trek</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="LEquipe.fr" site_id="lequipe">L&apos;Equipe</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Eurosport1.fr" site_id="eurosport-1">Eurosport 1</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Eurosport2.fr" site_id="eurosport-2">Eurosport 2</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Automotolachaine.fr" site_id="automoto">Automoto</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusSport.fr" site_id="canalplus-sport">Canal+ Sport</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusSport360.fr" site_id="canalplus-sport-360">Canal+ Sport 360</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusFoot.fr" site_id="canalplus-foot">Canal+ Foot</channel>
</channels>
<?xml version="1.0" encoding="UTF-8"?>
<channels>
<channel site="guidetnt.com" lang="fr" xmltv_id="TF1.fr" site_id="tf1">TF1</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France2.fr" site_id="france-2">France 2</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France3.fr" site_id="france-3">France 3</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France4.fr" site_id="france-4">France 4</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="France5.fr" site_id="france-5">France 5</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="M6.fr" site_id="m6">M6</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="arte.fr" site_id="arte">Arte</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="W9.fr" site_id="w9">W9</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TMC.fr" site_id="tmc">TMC</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TFX.fr" site_id="tfx">TFX</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TF1SeriesFilms.fr" site_id="tf1-series-films">TF1 Séries Films</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CStar.fr" site_id="cstar">CSTAR</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Cherie25.fr" site_id="cherie-25">Chérie 25</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="T18.fr" site_id="t18">T18</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="6ter.fr" site_id="6ter">6ter</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="RMCStory.fr" site_id="rmc-story">RMC STORY</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TV5MondeFranceBelgiumSwitzerlandMonaco.fr" site_id="tv5monde">TV5MONDE</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="LCP.fr" site_id="lcp-public-senat">LCP / Public Senat</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Gulli.fr" site_id="gulli">Gulli</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="DisneyChannel.fr" site_id="disney-channel">Disney Channel</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusKids.fr" site_id="canalplus-kids">Canal+ Kids</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="AB1.fr" site_id="ab1">AB1</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="MTV.fr" site_id="mtv">MTV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="E.fr" site_id="e-entertainment">E! Entertainment</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="13emeRue.fr" site_id="13eme-rue">13ème RUE</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TVBreizh.fr" site_id="tv-breizh"></channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PolarPlus.fr" site_id="polarplus">Polar+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ComediePlus.fr" site_id="comedieplus">Comedie+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ComedyCentral.fr" site_id="comedy-central">Comedy Central</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="BET.fr" site_id="bet">BET</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="MCM.fr" site_id="mcm">MCM</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ParamountChannel.fr" site_id="paramount-channel">Paramount Channel</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="WarnerTV.fr" site_id="warner-tv">Warner TV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="RTL9.lu" site_id="rtl9">RTL9</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Syfy.fr" site_id="syfy">Syfy</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Action.fr" site_id="action">Action</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="TCMCinema.fr" site_id="tcm">TCM</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlus.fr" site_id="canalplus">Canal+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusCinemas.fr" site_id="canalplus-cinema">Canal+ Cinéma(s)</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusGrandEcran.fr" site_id="canalplus-grand-ecran">Canal+ Grand écran</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusBoxOffice.fr" site_id="canalplus-box-office">Canal+ Box office</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusSeries.fr" site_id="canalplus-series">Canal+ Séries</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusDocs.fr" site_id="canalplus-docs">Canal+ Docs</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusOCS.fr" site_id="ocs">OCS</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusFrisson.fr" site_id="cineplus-frisson">Cine+ Frisson</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusClassic.fr" site_id="cineplus-classic">Cine+ Classic</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusFestival.fr" site_id="cineplus-festival">Cine+ Festival</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusEmotion.fr" site_id="cineplus-emotion">Cine+ Emotion</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CinePlusFamily.fr" site_id="cineplus-family">Cine+ Family</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="HistoireTV.fr" site_id="histoire">Histoire</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ToutelHistoire.fr" site_id="toute-l-histoire">Toute l&apos;histoire</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CrimeDistrict.fr" site_id="crime-district">Crime District</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="DiscoveryChannel.fr" site_id="discovery-channel">Discovery Channel</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="ScienceVieTV.fr" site_id="science-vie-tv">Science&amp;Vie TV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="RMCDecouverte.fr" site_id="rmc-decouverte">RMC Découverte</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PlanetePlus.fr" site_id="planeteplus">Planète+</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PlanetePlusCrime.fr" site_id="planeteplus-crime">Planète+ Crime</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="PlanetePlusAventure.fr" site_id="planeteplus-aventure">Planète+ Aventure</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="NationalGeographic.fr" site_id="national-geographic">National Geographic</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="NationalGeographicWild.fr" site_id="nat-geo-wild">Nat Geo Wild</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Animaux.fr" site_id="animaux">Animaux</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="UshuaiaTV.fr" site_id="ushuaia-tv">Ushuaia TV</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Trek.fr" site_id="trek">Trek</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="LEquipe.fr" site_id="lequipe">L&apos;Equipe</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Eurosport1.fr" site_id="eurosport-1">Eurosport 1</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Eurosport2.fr" site_id="eurosport-2">Eurosport 2</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="Automotolachaine.fr" site_id="automoto">Automoto</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusSport.fr" site_id="canalplus-sport">Canal+ Sport</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusSport360.fr" site_id="canalplus-sport-360">Canal+ Sport 360</channel>
<channel site="guidetnt.com" lang="fr" xmltv_id="CanalPlusFoot.fr" site_id="canalplus-foot">Canal+ Foot</channel>
</channels>

View File

@@ -1,338 +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,85 +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,21 +1,21 @@
# guidetnt.com
https://www.guidetnt.com/
### Download the guide
```sh
npm run grab --- --site=guidetnt.com
```
### Update channel list
```sh
npm run channels:parse --- --config=./sites/guidetnt.com/guidetnt.com.config.js --output=./sites/guidetnt.com/guidetnt.com.channels.xml
```
### Test
```sh
npm test --- guidetnt.com
```
# guidetnt.com
https://www.guidetnt.com/
### Download the guide
```sh
npm run grab --- --site=guidetnt.com
```
### Update channel list
```sh
npm run channels:parse --- --config=./sites/guidetnt.com/guidetnt.com.config.js --output=./sites/guidetnt.com/guidetnt.com.channels.xml
```
### Test
```sh
npm test --- guidetnt.com
```